java并发包系列(五)-ReentrantLock(二)

本文将介绍condition在ReentrantLock使用,进一步了解AQS基本原理。

在AQS文章中,我们详细分析了同步队列(sync队列),除了该队列我们知道AQS同是还维护一个条件队列(conditon队列)。在通过synchronized实现生产者/消费者模型中,我们会使用wait来挂起线程,表示当前不需要生产或者当前无法消费,使用notify/notifyall表示需要进行生产或者可以进行消费了。AQS作为java自己实现的锁一套框架,也提供了一样效果的操作类condition。如果说Lock替代了synchronized使用,那么Condition则替代了Object监视器方法的使用。

java并发包系列,五-ReentrantLock,二

以上示例代码展示了condition是如何使用的,如上图代码,定义了两个线程分别是“美丽公主”、“白马王子”,公主拿到了钥匙进入古城(lock.lock()),但是遇害然后沉睡(condition.await()),这个时候公主线程啥也做不了,需要等待王子线程的唤醒。王子线程拿到古城钥匙(lock.lock()),经过一番战斗,对公主进行了唤醒(发出信号signal()),于是公主获救,成功唤醒。这就是condition简单的使用示例。那我们就结合这个情景去分析condition底层实现。

先来看Condition接口方法,如下图,和Lock接口一样,Condition定义了一套应该有的接口,具体实现交给了同步器。里面分别有队线程的挂起和唤醒。

java并发包系列,五-ReentrantLock,二

对于挂起,其提供了5个方式,都是基于await()进行扩展的,比如对中断不明感的awaitUninterruptibly(),带有挂起时间限制的awaitNanos(long nanosTimeout)、await(long time, TimeUnit unit)、 awaitUntil(Date deadline),所以在以下源码分析中,将只会针对await() signal(),signalAll()进行分析。

示例代码中我们通过lock.newCondition()获取一个等待条件,Condition具体实现在AQS中,它被定义成了AQS的一个内部类ConditionObject,所以condition所具有的功能与Lock一样也是通过AQS代理实现的,Condition和Lock就是在外部进行了包装(所以理解了AQS,就理解了java内部实现锁定,这话不假)。先来看一下ConditionObject实现类的内部结构。

java并发包系列,五-ReentrantLock,二

ConditionObject有两个节点属性,firstWaiter和lastWaiter,分别表示条件队列的头节点和尾部节点,Node类结构和在AQS中分析的sync队列中Node是一个类,只是这里其用于条件等待队列。当有新的节点需要条件等待就会在lastWaiter节点插入,并更换lastWaiter;唤醒时候会从fistWaiter开始进行唤醒,并更换fistWaiter。后面两个int类型变量表示对中断是否敏感,REINTERRUPT表示从wait中若遇到中断进行恢复;THROW_IE则表示抛出中断异常。了解ConditionObject的内部结构后我们继续分析它是如何维护自己的条件等待队列的,如何对线程挂起和唤醒的。

java并发包系列,五-ReentrantLock,二

以上代码为await()实现源码,代码中加上了详细注释。addConditionWaiter()将当前节点增加到condition队列,节点waitStatus为CONDITION。await()进行锁释放动机很简单,当前线程被中断,需要释放手中的锁,以便其他线程获取,才有机会对自己进行signal。重点来分析代码中while循环问题。线程A在获取锁后,调用了await(),再将自己正真挂起前,需要释放自己手里的锁。释放完后当前线程从sync队列中删除,然后将自己挂起。那么问题来了线程A最终是需要继续竞争锁的,既然竞争锁就必须存在于队列中,但是await()并没有将当前线程的入队。其实入队操作是在signal中进行操作的,也只能在signal中,否则signal就没意义了。signal()主要作用不是唤醒线程,而是让线程加入到sync队列,让其有竞争锁的资格

java并发包系列,五-ReentrantLock,二

signal()方法代码首先通过isHeldExclusively()判断当前线程是否为AQS的正在运行线程,非则抛出异常。signal()要做的事情当前fisrtWaiter节点添加到sync队列和fisrtWaiter节点修改。最终实现是通过doSignal(first)来实现添加和节点修改的。

java并发包系列,五-ReentrantLock,二

如上注释,while循环体内是对fisrtWaiter节点修改,transferForSignal(node)将线程放入到sync队列中,其调用的enq(node)就是AQS那篇文章中向sync队列尾部增加节点的操作。

来看以上await对应得流程图,如下为线程调取await()的过程。

java并发包系列,五-ReentrantLock,二

signal()流程就相对简单,如下:

java并发包系列,五-ReentrantLock,二

这两副流程图结合着来看,流程一的循环中止条件是处于sync队列中,而流程二则是将等待节点放入sync队列。

signalAll()方法实现和signal()类似,只不过signal()将fistWaiter节点放入到sync队列中,而signalAll()则是从fistWaiter节点开始,将整个condition队列放入到sync队列中。对应的doSignalAll(node)如下很好理解:

java并发包系列,五-ReentrantLock,二

以上即为Condition原理实现分析,关于ReentrantLock分析就到这里,下篇文章将分析ReadWriteLock读写锁的具体实现和应用。

如有错漏之处,欢迎关注头条号/微信公众号"程序员杂谈",留言指正!