【JUC学习】——Lock和Synchronized使用及二者区别

众所周知,在JUC中常见的锁就是Lock和Synchronized了,主要是用于并发多线程的同步执行问题,用于在许多线程执行时对资源的限制​。锁通常需要硬件支持才可以有效实施。这种支持通常采用一个或多个原子指令,测试单个线程是否空闲。
Lock是显式加锁,锁释放。而synchronized是隐式锁,出了作用域自动释放,Lock只有代码块锁,synchronized有代码块锁和方法锁。使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类ReentrantLock(). )
优先使用顺序:Lock > 同步代码块 (已经进入方法体,分配了相应资源) > 同步方法(在方法体之外)
由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字。包括synchronized方法和synchronized块:synchronized方法控制对 对象 的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
使用synchronized缺陷: 将一个大方法申明为synchronized将会影响效率,一般只是将方法中的需要保证同步的代码放到synchronized块中,同步方法中的同步监视器就是this,对象本身或者class。
下面看一下使用小案例:
1、抢票小案例(不安全状态)

class Tickets1 implements Runnable{
    private  Integer ticketNums=10;
    private  boolean flag=true;
    @Override
    public void run() {
        while(flag){  
            try {
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

     void buy() throws InterruptedException {
        if(ticketNums<=0){
            System.out.println("抢票停止了!");
            flag=false;
            return;
        }else{
            Thread.sleep(100); //防止一个线程就抢完了
            System.out.println(Thread.currentThread().getName()+"抢到了第"+ticketNums--+"张票!");
        }
    }

    public static void main(String[] args) {
        Tickets1 tickets=new Tickets1();
        new Thread(tickets,"张三").start();
        new Thread(tickets,"李四").start();
        new Thread(tickets,"王五").start();
    }
}

在这里插入图片描述
显然这个案例在多线程环境下是不安全的,解决办法就是使用线程同步机制,限制线程对资源的使用。下面采用Java内置关键字synchronized来保证线程安全。

class Tickets1 implements Runnable{
    private int ticketNums=10;
    private boolean flag=true;
    @Override
    public void run() {
        while(flag){
            try {
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    void buy() throws InterruptedException { //或者直接锁方法上synchronized void
        synchronized (this){
            if(ticketNums<=0){
                System.out.println("抢票停止了!");
                flag=false;
                return;
            }else{
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName()+"抢到了第"+ticketNums--+"张票!");
            }
        }
    }

    public static void main(String[] args) {
        Tickets1 tickets=new Tickets1();
        new Thread(tickets,"张三").start();
        new Thread(tickets,"李四").start();
        new Thread(tickets,"王五").start();
    }
}

在这里插入图片描述
同理Lock版本如下,我们一般使用Lock的实现类ReentrantLock来创建lock锁(可重入锁:也称递归锁,再同一线程的外层方法获取锁的时候,在进入内层方法自动获取锁。可重入锁一个好处是在一定程度上避免死锁),一个boolean参数fair来确定是否使用公平策略,默认是false非公平策略:
关于ReentrantLock可参考:一文彻底理解ReentrantLock可重入锁的使用Java多线程系列——深入重入锁ReentrantLock

class Tickets implements Runnable{
    private  Integer ticketNums=10;
    private  boolean flag=true;
    private  final ReentrantLock lock=new ReentrantLock();//可重入锁
    @Override
    public void run() {
        while(flag){
            buy();
        }
    }

    /**
     *Lock是显式锁、synchronized是隐式锁,出了作用域自动释放,Lock只有代码块锁,synchronized有代码块锁和方法锁
     * 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
     * 优先使用顺序:Lock > 同步代码块 (已经进入方法体,分配了相应资源) > 同步方法(在方法体之外)
     */
    void  buy() {
        lock.lock(); //显式加锁,别写在try中,万一获取锁时候超时了就会触发捕捉异常,然后进入finally直接解锁
        try{
            if(ticketNums<=0){
                System.out.println("抢票停止了!");
                flag=false;
                return;
            }else{
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName()+"抢到了第"+ticketNums--+"张票!");
            }
        }catch (Exception ex){
        }finally {
            lock.unlock();//显式解锁,如果同步代码块有异常,要将unlock()写入finally语句块
        }
    }

    public static void main(String[] args) {
        Tickets tickets=new Tickets();
        new Thread(tickets,"张三").start();
        new Thread(tickets,"李四").start();
        new Thread(tickets,"王五").start();
    }
}

在OOP思想中,类中只包含属性和方法,因此我们可以采用lambda表达式简化该类型的例子。

public class Synchronized {
    public static void main(String[] args) {
        Tickets tickets=new Tickets();
        //Thread.holdsLock(tickets);//判断是否获得了tickets对象的锁
        //java.lang.Thread 中有一个方法叫做holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁
        new Thread(()->{  //因为Thread第一个参数为Runnable接口,Runnable只有一个run方法,是一个函数式接口,因此可以使用Lambda表达式
            for (int i = 0; i < 30; i++) tickets.saleLock();
        },"A").start();
        new Thread(()->{
            for (int i = 0; i < 40; i++) tickets.saleLock();
        },"B").start();
        new Thread(()->{
            for (int i = 0; i < 40; i++) tickets.saleLock();
        },"C").start();
    }
}


/**
 * OOP思想:属性和方法
 * 如果Tickets继承Runnable重写run方法,会增加耦合性
 */
class Tickets{
    int tickNums=30;
    public synchronized void sale(){
        if(tickNums>0){
            System.out.println(Thread.currentThread().getName()+"抢到了第"+(tickNums--)+"张票----->"+"当前剩余"+tickNums+"张票!");
        }else{
            System.out.println("票已经售完!");
        }
    }
    public void saleLock(){
        Lock lock=new ReentrantLock(); //可重入锁(可以多次获取同一个锁,释放也需要多次释放),boolean参数决定是否是公平锁
        lock.lock(); //加锁
        //lock.tryLock();//尝试获取锁,
        try{
            if(tickNums>0){
                System.out.println(Thread.currentThread().getName()+"抢到了第"+(tickNums--)+"张票----->"+"当前剩余"+tickNums+"张票!");
            }else{
                System.out.println("票已经售完!");
            }
        }catch (Exception ex){
            ex.printStackTrace();
        }finally {
            lock.unlock(); //释放锁,否则可能导致死锁发生
        }
    }
}

更多使用可参见jdk文档下java.util.concurrent内容。
Lock和Synchronized区别:

1、Synchronized是Java的内置关键字,Lock是一个java接口。
2、Synchronized无法判断获取锁的状态,Lock可以通过java.lang.Thread 中的holdsLock()方法判断是否获得某个对象的锁。
3、Synchronized会自动释放锁,Lock必须手动释放锁,否则可能产生死锁。
4、如果一个线程获得了锁,对于Synchronized来说其他需要使用这个资源的线程就只能等待下去,如果获得了锁的线程进入阻塞状态,其他线程依旧会等待,Lock锁就不一定会一直等待下去,可以通过tryLock尝试获取锁。
5、Synchronized可重入锁,不可以中断,非公平的,Lock可重入锁,可以判断锁的状态,是否公平可以通过fair参数进行设置,相对而言比较灵活。
6、Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码!

谈谈synchronized与ReentrantLock的区别?

① 底层实现上来说,synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),对象只有在同步块或同步方法中才能调用wait/notify方法,ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁。 synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、轻量级锁、向OS申请重量级锁,ReentrantLock实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。
② 是否可手动释放:
synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活。
③ 是否可中断
synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成; ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时时间或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。
④ 是否公平锁
synchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁。
⑤ 锁是否可绑定条件Condition
synchronized不能绑定; ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒,而不是像synchronized通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程。
⑥ 锁的对象
synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

童话ing

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值