ReentrantLock和Synchronized的区别和原理

小菜鸡准备找工作啦,整理面试常见到的问题整理一下。

ReentrantLock和Synchronized的区别:

相似点:

两个都是可重入锁,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善)。

功能区别:

相同点:

             1.它们都是加锁方式同步;

              2.都是重入锁;

             3. 阻塞式的同步;也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善);      

不同点:

  • 比较方面 SynChronized ReentrantLock(实现了 Lock接口)
    原始构成 它是java语言的关键字,是原生语法层面的互斥,需要jvm实现 它是JDK 1.5之后提供的API层面的互斥锁类
    实现 通过JVM加锁解锁 api层面的加锁解锁,需要手动释放锁。
    代码编写 采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用,更安全, 而ReentrantLock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。需要lock()和unlock()方法配合try/finally语句块来完成,
    灵活性 锁的范围是整个方法或synchronized块部分 Lock因为是方法调用,可以跨方法,灵活性更大
    等待可中断

    不可中断,除非抛出异常(释放锁方式:

            1.代码执行完,正常释放锁;

            2.抛出异常,由JVM退出等待)

    持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,(方法:

                     1.设置超时方法 tryLock(long timeout, TimeUnit unit),时间过了就放弃等待;

                      2.lockInterruptibly()放代码块中,调用interrupt()方法可中断,而synchronized不行)
     

    是否公平锁 非公平锁 两者都可以,默认公平锁,构造器可以传入boolean值,true为公平锁,false为非公平锁,
    条件Condition   通过多次newCondition可以获得多个Condition对象,可以简单的实现比较复杂的线程同步的功能.
    提供的高级功能   提供很多方法用来监听当前锁的信息,如:
     getHoldCount() 
     getQueueLength()
     isFair()
    isHeldByCurrentThread()
    isLocked()
    便利性 Synchronized的使用比较方便简洁,由编译器去保证锁的加锁和释放 需要手工声明来加锁和释放锁,
    适用情况 资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronize,另外可读性非常好 ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。

    Synchronized原理:

    Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。

    monitorenter :

    每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

    1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

    2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

    3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

    monitorexit:

    执行monitorexit的线程必须是objectref所对应的monitor的所有者。

    指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

    详细参考这个博主写的,很详细:https://blog.csdn.net/javazejian/article/details/72828483

    ReentrantLock

    由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下3项:

            1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。通过lock.lockInterruptibly()来实现这个机制。

            2.公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。

    公平锁、非公平锁的创建方式:

    //创建一个非公平锁,默认是非公平锁Lock lock = new ReentrantLock();Lock lock = new ReentrantLock(false); //创建一个公平锁,构造传参trueLock lock = new ReentrantLock(true);

            3.锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象。ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

    ReenTrantLock实现的原理:

    CAS+CLH队列来实现。它支持公平锁和非公平锁,两者的实现类似。

    CAS:Compare and Swap,比较并交换。CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现。

    CLH队列:带头结点的双向非循环链表

    AbstractQueuedSynchronizer

    ReentrantLock实现的前提就是AbstractQueuedSynchronizer,简称AQS,是java.util.concurrent的核心,CountDownLatch、FutureTask、Semaphore、ReentrantLock等都有一个内部类是这个抽象类的子类。先用两张表格介绍一下AQS。第一个讲的是Node,由于AQS是基于FIFO队列的实现,因此必然存在一个个节点,Node就是一个节点,Node里面有:

    属    性 定    义
    Node SHARED = new Node() 表示Node处于共享模式
    Node EXCLUSIVE = null 表示Node处于独占模式
    int CANCELLED = 1 因为超时或者中断,Node被设置为取消状态,被取消的Node不应该去竞争锁,只能保持取消状态不变,不能转换为其他状态,处于这种状态的Node会被踢出队列,被GC回收
    int SIGNAL = -1 表示这个Node的继任Node被阻塞了,到时需要通知它
     int CONDITION = -2 表示这个Node在条件队列中,因为等待某个条件而被阻塞 
    int PROPAGATE = -3 使用在共享模式头Node有可能处于这种状态, 表示锁的下一次获取可以无条件传播
     int waitStatus 0,新Node会处于这种状态 
     Node prev 队列中某个Node的前驱Node 
     Node next 队列中某个Node的后继Node 
    Thread thread 这个Node持有的线程,表示等待锁的线程
    Node nextWaiter 表示下一个等待condition的Node

    看完了Node,下面再看一下AQS中有哪些变量和方法:

    属性/方法 含    义
    Thread exclusiveOwnerThread 这个是AQS父类AbstractOwnableSynchronizer的属性,表示独占模式同步器的当前拥有者
    Node 上面已经介绍过了,FIFO队列的基本单位
    Node head FIFO队列中的头Node
    Node tail FIFO队列中的尾Node
    int state 同步状态,0表示未锁
    int getState() 获取同步状态
    setState(int newState) 设置同步状态
    boolean compareAndSetState(int expect, int update)  利用CAS进行State的设置 
     long spinForTimeoutThreshold = 1000L 线程自旋等待的时间 
    Node enq(final Node node)  插入一个Node到FIFO队列中 
    Node addWaiter(Node mode) 为当前线程和指定模式创建并扩充一个等待队列
    void setHead(Node node) 设置队列的头Node
    void unparkSuccessor(Node node) 如果存在的话,唤起Node持有的线程
    void doReleaseShared() 共享模式下做释放锁的动作
    void cancelAcquire(Node node) 取消正在进行的Node获取锁的尝试
    boolean shouldParkAfterFailedAcquire(Node pred, Node node) 在尝试获取锁失败后是否应该禁用当前线程并等待
    void selfInterrupt() 中断当前线程本身
    boolean parkAndCheckInterrupt() 禁用当前线程进入等待状态并中断线程本身
    boolean acquireQueued(final Node node, int arg) 队列中的线程获取锁
    tryAcquire(int arg) 尝试获得锁(由AQS的子类实现它
    tryRelease(int arg) 尝试释放锁(由AQS的子类实现它
    isHeldExclusively() 是否独自持有锁
    acquire(int arg) 获取锁
    release(int arg) 释放锁
    compareAndSetHead(Node update) 利用CAS设置头Node
    compareAndSetTail(Node expect, Node update) 利用CAS设置尾Node
    compareAndSetWaitStatus(Node node, int expect, int update) 利用CAS设置某个Node中的等待状态

    上面列出了AQS中最主要的一些方法和属性。整个AQS是典型的模板模式的应用,设计得十分精巧,对于FIFO队列的各种操作在AQS中已经实现了,AQS的子类一般只需要重写tryAcquire(int arg)和tryRelease(int arg)两个方法即可

     

    ReentrantLock锁的架构

    ReentrantLoc的架构相对简单,主要包括一个Sync的内部抽象类以及Sync抽象类的两个实现类。上面已经说过了Sync继承自AQS,他们的结构示意图如下:

    ReentrantLock根据传入构造方法的布尔型参数实例化出Sync的实现类FairSync和NonfairSync,分别表示公平的Sync和非公平的Sync。由于ReentrantLock我们用的比较多的是非公平锁,所以看下非公平锁是如何实现的。假设线程1调用了ReentrantLock的lock()方法,那么线程1将会独占锁,整个调用链十分简单:

    第一个获取锁的线程就做了两件事情:

    1、设置AbstractQueuedSynchronizer的state为1

    2、设置AbstractOwnableSynchronizer的thread为当前线程

    这两步做完之后就表示线程1独占了锁。然后线程2也要尝试获取同一个锁,在线程1没有释放锁的情况下必然是行不通的,所以线程2就要阻塞。那么,线程2如何被阻塞?看下线程2的方法调用链,这就比较复杂了:

    参考:https://www.cnblogs.com/xrq730/p/4979021.html

    https://blog.csdn.net/u011194983/article/details/79932650

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(ReentrantLock和Synchronized的区别和原理)