ReentrantLock AQS源码

前言

    之前讲过synchronized关键字在JDK1.7之前是一把重量级的锁,那时JVM还未对synchronized关键字进行优化,所以synchronized会调用操作系统的函数实现加锁和解锁。而在JDK1.7后JVM对其进行优化,synchronized可以通过自旋达到一把轻量级的锁,在JVM级别就可以得到实现,无须再调用操作系统函数。而在此期间并发大神Doug Lea就写出了很多的并发工具类,ReentrantLock就是其中的一种,ReentrantLock所实现的功能也要比synchronized丰富,我们都知道ReentrantLock的实现底层还是依赖于AQS(Abstract Queued Synchronizer)同步队列器实现,下面主要分析下ReentrantLock的加锁和解锁过程


ReentrantLock,Synchronized功能区别

    ReentrantLock支持lock()加锁,支持超时加锁,支持公平和非公平锁,支持lockInterruptibly线程中断响应,Synchronized关键字所能支持的功能就比较单一,Synchronized的好处就是不需要我们手动的加锁和释放锁,JVM帮我们实现了。而ReentrantLock就需要我们手动的加锁和释放锁,如果一旦锁没有得到释放,那么所造成的影响就比较大了,ReentrantLock和Synchronized都是可重入锁


ReentrantLock所使用到的重要技术点

1. 自旋(for死循环获取锁)
2. CAS操作(通过CAS操作更新state锁状态,更新成功表示锁获取成功)
3. 依赖LockSupport.park()线程等待, LockSupport.unpark线程唤醒

AQS(AbstractQueuedSynchronizer)类实现的主要代码

private transient volatile Node head; //队列的头部节点
private transient volatile Node tail; //队列的尾部节点
private volatile int state; //锁的状态,state=0表示自由状态,没有被线程占用,state > 0表示被线程已经占用

    队列节点Node的主要实现代码

volatile Node prev; //上一个节点
volatile Node next; //下一个节点
volatile Thread thread; //节点中的线程
volatile int waitStatus; //表示下一个节点的状态,是取消还是睡眠

    AQS中的队列就有点类似于图中, head头部指向的节点的thread是null,队首的节点不参与排队竞争锁资源


ReentrantLock公平和非公平加锁区别

    公平锁加锁流程

    重点分析下tryAcquire方法获取锁

    我们可以看到线程去获取锁的时候会去判断当前线程是否需要进行排队操作,而后续继续分析获取锁失败(CAS操作失败)怎样去入队的


    非公平锁加锁流程

    我们可以看到非公平锁加锁最后会调用到nonfairTryAcquire方法,而nonfairTryAcquire方法跟公平锁的tryAcquire方法有点类似,只不过nonfairTryAcquire方法少了hasQueuedPredecessors方法,判断是否需要入队,因为是非公平的,没有排队的概念。

    以上就是公平锁和非公平锁加锁的流程,下面来分析加锁失败ReentrantLock是怎样去处理的,公平锁和非公平锁加锁失败后,入队列的流程都是一样的,都是依赖于AQS去实现的


AQS加锁失败,线程入队流程

    前面我们分析到tryAcquire方法加锁失败,返回false,取反则是true。也就是会执行到&&后面的一个条件acquireQueued(addWaiter(Node.EXCLUSIVE), arg),这里首先分析一下addWaiter方法,线程入队操作

    来看一下addWaiter方法的处理流程

    首先会将当前线程thread初始化成一个Node,然后判断尾部节点是否为空,如果为空,则直接执行enq方法,如果不能空,则改变尾部节点和当前节点Node的prev和next指针,通过compareAndSetTail操作将Node节点添加到尾部,这里可能会出现CAS操作失败,如果失败,则也要执行enq方法,如果添加尾部成功,则直接返回,这里要注意一个点addWaiter方法和后面要分析的enq方法都是先设置Node.prev指针。体现在后面unlock()释放锁唤醒队列线程的时候是从队尾向队首找waitStatus<=0的节点,如果是通过next指针从队首往队尾去找waitStatus<=0的节点,就有可能会出现next指针还未进行设置,找不到下一个节点


    enq方法处理流程

    我们可以看到enq方法的流程是通过for死循环+CAS操作完成设置尾部节点,首先会检测AQS队列是否进行初始化,如果未进行初始化,则会初始化一个Node, Node中的thread=null的节点,head和tail指针一起指向Node,初始化完成之后再进行一次循环则会通过改变尾部节点和当前节点Node的prev和next指针,通过compareAndSetTail操作将Node节点添加到尾部,直到CAS操作成功返回

    以上addWaiter和enq方法就是入队操作,逻辑还是比较清晰的,入完队列之后,要开始进行LockSupport.park操作,看ReentrantLock是怎么进行park操作的,下面看一下acquireQueued处理流程


AQS的acquireQueued流程

    前面分析到入队完成,再执行acquireQueued方法,看一下acquireQueued的处理流程

    可以发现acquireQueued中也是有个for(;;)死循环来一直获取锁和park操作,还会判断自己的上一个节点是否是头节点,如果是头节点的话,会再次尝试去获取锁。如果还是获取锁失败,则会判断自己是否需要进行park操作,也就是判断上一个节点的waitStatus==-1是否成立,如果成立,则会调用LockSupport.park()进行等待

    至此,ReentrantLock的整个加锁流程就分析完成了,可以总结出ReentrantLock的加锁使用到大量的自旋,CAS操作,最后进行park操作。最后流程图我们看到还有一行代码Thread.interrupted(),不知道起到什么作用?我们前面讲到ReentrantLock还支持方法lockInterruptibly线程中断响应


ReentrantLock支持的lockInterruptibly方法

    我们可以看到lockInterruptibly调用的acquireInterruptibly方法,会首先判断一下Thread.interrupted()线程的中断状态,如果设置了中断标识,则会抛出异常,我们可以try catch异常,拿到响应。

    我们发现acquireInterruptibly再调用的doAcquireInterruptibly方法跟我们所分析的acquireQueued有点类似,后面同样调用了parkAndCheckInterrupt方法,这里发现Thread.interrupted()是中断状态的话,doAcquireInterruptibly会抛出异常,来响应中断。而acquireQueued方法不会抛出异常而是将interrupted设置为true,最后返回的是interrupted

    而整个acquireQueued返回true时,就会执行selfInterrupt方法,将线程至为Thread.currentThread().interrupt()中断状态,这里其实就是为了还原用户行为,因为用户将线程设置为interrupt状态,而acquireQueued跟doAcquireInterruptibly方法调用了同样的parkAndCheckInterrupt方法,又会将thread的interrupt状态反置回来,所以后面又调用了selfInterrupt方法,将用户的行为还原回来,这里可能是大神Doug Lea不想重新再写一个方法,所以让他们两者进行了复用哈哈哈


ReentrantLock释放锁的流程

    加锁流程分析完成之后,释放锁的流程就比较简单了,主要注意的代码是

    首先将当前节点Node的waitStatus置为0,再获取到Node节点的下一个节点,如果s.waitStatus>0说明被取消, 则从尾部向头部进行查找waitStatus<=0的节点,前面我们讲过为什么要从尾部向头部进行查找,因为是先设置prev上一个节点指针的值

    还需要注意一点的是为什么用上一个节点的waitStatus=-1来表示下一个节点的睡眠状态,这里是为了当释放锁时,可以根据节点waitStatus的状态来唤醒下一个节点


AQS是否需要排队操作hasQueuedPredecessors

    我觉得这段代码是大神Doug Lea将程序员的精神体现得流漓尽致,就简单的几行代码,将是否需要排队的情况都包含在里面,首先我们可以分析下,那几种情况线程是不需要排队的

1. 队列未初始化,也就是head = tail = null, h != t返回false,表示不需要排队
2. 队列初始化了
	2.1 如果队列元素 = 1, 表明head == tail,
	也就是一个虚拟节点,也就是前面分析到的thread=null,h != t返回false,不需要排队
	2.2 如果队列元素 > 1, 表示 h != t返回true,执行后面的(s = h.next) ==null返回false,
        再执行s.thread != Thread.currentThread()返回的是true。
        h != t返回true和s.thread != Thread.currentThread()也返回true,
        两个条件都满足,表明需要进行排队

    我们可以看到相当于一行代码,就将三种情况包含在里面


总结

    前面主要分析了ReentrantLock的加锁和入队,释放锁的流程,可以看到主要还是依赖于AQS同步队列器去实现,在concurrent包的并发工具类,有很大一部分都是依赖于AQS去实现的,像CountDownLatch,Semaphore里有个静态内部类Sync继承自AQS,像CyclicBarrier则是使用到了ReentrantLock和Condition,间接的采用AQS实现

posted @ 2020-08-09 18:37  半分、  阅读(402)  评论(0编辑  收藏  举报