ReentrantLock 详解

为君一笑 提交于 2020-01-08 10:02:21

一、初始ReentrantLock

        ReentrantLock 位于 java.util.concurrent.locks 包下,它实现了 Lock 接口和 Serializable 接口。ReentrantLock 是一把可重入锁互斥锁,它具有与 synchronized 关键字相同的含有隐式监视器锁(monitor)的基本行为和语义,但是它比 synchronized 具有更多的方法和功能。

                        

二、 ReentrantLock的基本方法

1. 构造函数

        ReentrantLock 类中带有两个构造函数,一个是默认的构造函数,不带任何参数;一个是带有 fair 参数的构造函数。

public ReentrantLock() {
  sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
  sync = fair ? new FairSync() : new NonfairSync();
}

       第二个构造函数也是判断 ReentrantLock 是否是公平锁的条件,如果 fair 为 true,则会创建一个公平锁的实现,也就是    new FairSync(),如果 fair 为 false,则会创建一个 非公平锁的实现,也就是 new NonfairSync(),默认的情况下创建的是非公平锁。FairSyncNonfairSync 都是 ReentrantLock 的内部类,即继承于 Sync 类。在多线程尝试加锁时,如果是公平锁,那么锁获取的机会是相同的。否则,如果是非公平锁,那么 ReentrantLock 则不会保证每个锁的访问顺序

                               

2. 公平锁的加锁(lock)流程详解

        通常情况下,使用多线程访问公平锁的效率会非常低(通常情况下会慢很多),但是 ReentrantLock 会保证每个线程都会公平的持有锁,线程饥饿的次数比较小。锁的公平性并不能保证线程调度的公平性。下图为公平锁和非公平锁的加锁流程。

                         
下图为acquire方法的三条主要流程 :

                         

首先是第一条路线,tryAcquire 方法,顾名思义尝试获取,也就是说可以成功获取锁,也可以获取锁失败。它会调用AQS的的方法,由于ReentrantLock 实现了 AQS 接口,所以调用的是 ReentrantLock 的 tryAcquire 方法。

               

       首先会取得当前线程,然后去读取当前锁的同步状态。如果判断同步状态是 0 的话,就证明是无锁的。如果是无锁(也就是没有加锁),说明是第一次上锁,首先会先判断一下队列中是否有比当前线程等待时间更长的线程hasQueuedPredecessors);然后通过 CAS 方法原子性的更新锁的状态,CAS 方法更新的要求涉及三个变量,currentValue(当前线程的值),expectedValue(期望更新的值),updateValue(更新的值).

if(currentValue == expectedValue){
  currentValue = updateValue
}

        CAS 通过 C 底层机制保证原子性。如果既没有排队的线程而且使用 CAS 方法成功的把 0 -> 1 (偏向锁),那么当前线程就会获得偏向锁,记录获取锁的线程为当前线程。然后我们看 else if 逻辑,如果读取的同步状态是1,说明已经线程获取到了锁,那么就先判断当前线程是不是获取锁的线程,如果是的话,记录一下获取锁的次数 + 1,也就是说,只有同步状态为 0 的时候是无锁状态。如果当前线程不是获取锁的线程,直接返回 false。

      acquire 方法会先查看同步状态是否获取成功,如果成功则方法结束返回,也就是 !tryAcquire == false ,若失败则先调用 addWaiter方法再调用 acquireQueued 方法

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

第二条路线 addWaiter:

                  

       这里首先把当前线程和 Node 的节点类型进行了封装,Node节点的类型有两种,EXCLUSIVE 和 SHARED ,前者为独占模式,后者为共享模式。首先会进行 tail 节点的判断,有没有尾节点,其实没有头节点也就相当于没有尾节点,如果有尾节点,就会原子性的将当前节点插入同步队列中,再执行enq入队操作,入队操作相当于原子性的把节点插入队列中。如果当前同步队列尾节点为null,说明当前线程是第一个加入同步队列进行等待的线程。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

第三条路线 acquireQueued

                    

        主要会有两个分支判断,首先会进行无限循环中,循环中每次都会判断给定当前节点的先驱节点,如果没有先驱节点会直接抛出空指针异常,直到返回 true。然后判断给定节点的先驱节点是不是头节点,并且当前节点能否获取独占式锁,如果是头节点并且成功获取独占锁后,队列头指针用指向当前节点,然后释放前驱节点。如果没有获取到独占锁,就会进入 shouldParkAfterFailedAcquire 和 parkAndCheckInterrupt 方法中,下图为两个方法的源码:

   shouldParkAfterFailedAcquire 方法主要逻辑是使用compareAndSetWaitStatus(pred, ws, Node.SIGNAL)使用CAS将节点状态由 INITIAL 设置成 SIGNAL,表示当前线程阻塞。当 compareAndSetWaitStatus 设置失败则说明 shouldParkAfterFailedAcquire 方法返回 false,然后会在 acquireQueued 方法中死循环中会继续重试,直至compareAndSetWaitStatus 设置节点状态位为 SIGNAL 时 shouldParkAfterFailedAcquire 返回 true 时才会执行方法 parkAndCheckInterrupt 方法。                                                                                                                                          

   parkAndCheckInterrupt 该方法的关键是会调用 LookSupport.park 方法,该方法是用来阻塞当前线程。

        所以 acquireQueued 主要做了两件事情:如果当前节点的前驱节点是头节点,并且能够获取独占锁,那么当前线程能够获得锁该方法执行结束退出。如果获取锁失败的话,先将节点状态设置成 SIGNAL,然后调用 LookSupport.park 方法使得当前线程阻塞。如果 !tryAcquire  acquireQueued 都为 true 的话,则打断当前线程。公平锁的加锁主要流程如下:

                          

 3. 非公平锁的加锁流程详解

        非公平锁的加锁步骤和公平锁的步骤只有两处不同,一处是非公平锁在加锁前会直接使用 CAS 操作设置同步状态,如果设置成功,就会把当前线程设置为偏向锁的线程;一处是 CAS 操作失败执行 tryAcquire 方法,读取线程同步状态,如果未加锁会使用 CAS 再次进行加锁,不会等待 hasQueuedPredecessors 方法的执行,来达到只要线程释放锁就会加锁的目的。

                          

                          

4. lockInterruptibly 以可中断的方式获取锁 

       lockInterruptibly 的中文意思为如果没有被打断,则获取锁。如果没有其他线程持有该锁,则获取该锁并立即返回,将锁保持计数设置为1。如果当前线程已经持有锁,那么此方法会立刻返回并且持有锁的数量会+1。如果锁是由另一个线程持有的,则出于线程调度目的,当前线程将被禁用,并处于休眠状态,直到发生以下两种情况之一

  • 锁被当前线程持有

  • 一些其他线程打断了当前线程

如果当前线程获取了锁,则锁保持计数将设置为1。

如果当前线程发生了如下情况:

  • 在进入此方法时设置了其中断状态

  • 当获取锁的时候发生了中断(Thread.interrupt)

那么当前线程就会抛出InterruptedException 并且当前线程的中断状态会清除。

                          

5. tryLock 尝试加锁

    仅仅当其他线程没有获取这把锁的时候获取这把锁。

                            

6. unlock 解锁流程

    unlock 和 lock 是一对情侣,它们分不开彼此,在调用 lock 后必须通过 unlock 进行解锁。如果当前线程持有锁,在调用 unlock 后,count 计数将减少。如果保持计数为0就会进行解锁。如果当前线程没有持有锁,在调用 unlock 会抛出 IllegalMonitorStateException 异常。

                           

三、Synchronzied 和 Lock 的主要区别

  • 存在层面:Syncronized 是Java 中的一个关键字,存在于 JVM 层面,Lock 是 Java 中的一个接口

  • 锁的释放条件:1. 使用Syncronized获取锁的线程执行完同步代码后,会自动释放;2. 线程发生异常时,JVM会让线程释放锁;Lock 必须在 finally 关键字中释放锁,不然容易造成线程死锁。

  • 锁的获取: 在 Syncronized 中,假设线程 A 获得锁,B 线程等待。如果 A 发生阻塞,那么 B 会一直等待。在 Lock 中,会分情况而定,Lock 中有尝试获取锁的方法,如果尝试获取到锁,则不用一直等待。

  • 锁的状态:Synchronized 无法判断锁的状态,Lock 则可以判断。

  • 锁的类型:Synchronized 是可重入,不可中断,非公平锁;Lock 锁则是可重入,可判断,可公平锁。

  • 锁的性能:Synchronized 适用于少量同步的情况下,性能开销比较大。Lock 锁适用于大量同步阶段:

    Lock 锁可以提高多个线程进行读的效率(使用 readWriteLock)

  • 在竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态(jdk1.6中对Synchronized进行可大量的优化,二者的性能已经相近);

  • ReetrantLock 提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。

================================================================================================

简化版的加锁流程:

  • 如果 lock 加锁设置成功,设置当前线程为独占锁的线程;

  • 如果 lock 加锁设置失败,还会再尝试获取一次锁数量,如果锁数量为0,再基于 CAS 尝试将 state(锁数量)从0设置为1一次,如果设置成功,设置当前线程为独占锁的线程;如果锁数量不为0或者上面的尝试又失败了,查看当前线程是不是已经是独占锁的线程了,如果是,则将当前的锁数量+1;如果不是,则将该线程封装在一个Node内,并加入到等待队列中去。等待被其前一个线程节点唤醒 。

来源:Java建设者

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!