有图解有案例,我终于把Condition的原理讲透彻了

网站建设3年前发布
40 0 0

哈喽大家好,我是阿Q!,​​20张图图解ReentrantLock加锁解锁原理​​​文章一发,便引发了大家激烈的讨论,更有小伙伴前来弹窗:平时加解锁都是直接使用Synchronized​关键字来实现的,简单好用,为啥还要引用ReentrantLock呢?,为了解决小伙伴的疑问,我们来对两者做个简单的比较吧:,两者都是“可重入锁”,即当前线程获取到锁对象之后,如果想继续获取锁对象还是可以继续获取的,只不过锁对象的计数器进行“+1”操作就可以了。,综上所述,ReentrantLock​还是有区别于Synchronized的使用场景的,今天我们就来聊一聊它的多路选择通知功能。,没有实战的“纸上谈兵”都是扯淡,今天我们反其道而行,先抛出实战Demo。,加油站为了吸引更多的车主前来加油,在加油站投放了自动洗车机来为加油的汽车提供免费洗车服务。我们规定汽车必须按照“加油->洗车->驶离”的流程来加油,等前一辆汽车驶离之后才允许下一辆车进来加油。,首先创建锁对象并生成三个Condition,然后声明加油、清洗、驶离的方法,并规定加完油之后去洗车并驶离加油站,其中await​为等待方法,signal为唤醒方法。,最后我们来定义main方法,模拟一下3辆车同时到达加油站的场景,使用是不是很丝滑?为了加深大家对Condition​的理解,接下来我们用图解的方式分析一波Condition的原理~,大家都看到了,上边的案例都是围绕Condition​来操作的,那什么是Condition​呢?Condition是一个接口,里边定义了线程等待和唤醒的方法。,20230306135044f57204834571a672e41750154c007d7efbbf71483,代码中调用的lock.newCondition()​实际调用的是Sync​类中的newCondition​方法,而ConditionObject​就是Condition的实现类。,我们发现它处于AQS​的内部,没法直接实例化,所以需要配合ReentrantLock来使用。,ConditionObject,2023030613492852b9af01184bc8edea721078720aa47dec4ac2960,ConditionObject​内部维护了一个基于Node的FIFO​单向队列,我们把它称为等待队列。firstWaiter​指向首节点,lastWaiter​指向尾节点,Node​中的nextWaiter​指向队列中的下一个元素,并且等待队列中节点的waitStatus都是-2。,了解了ConditionObject​的数据结构之后,我们就从源码角度来图解一下ReentrantLock的等待/唤醒机制。,await,首先找到AQS​类中await的源码,如果线程中断,清除中断标记并抛出异常。,查看addConditionWaiter,该方法的作用是将当前线程封装成node加入等待队列尾部,首先将t指向尾节点,如果尾节点不为空并且它的waitStatus!=-2,则将不处于等待状态的结点从等待队列中移除,并且将t指向新的尾节点。,将当前线程封装成waitStatus为-2的节点追加到等待队列尾部。,如果尾节点为空,则队列为空,将首尾节点都指向当前节点。,20230306134928f295bcf13c41b73359a735765788c18bbe7eec720,如果尾节点不为空,证明队列中有其他节点,则将当前尾节点的nextWaiter指向当前节点,将当前节点置为尾节点。,20230306134929e1e93b90650f5825f6b256bae710006c3dcb98185,接着我们来查看下unlinkCancelledWaiters()方法——将不处于等待状态的结点从等待队列中移除。,t为当前节点,trail​为t的前驱节点,next为t的后继节点。,while​方法会从首节点顺着等待队列往后寻找waitStatus!=-2​的节点,将当前节点的nextWaiter置为空。,如果当前节点的前驱节点为空,代表当前节点为首节点,则将next设置为首节点;,20230306134929f963da3133e57c869dc8958c43b26a89ecde32728,如果不为空,则将前驱节点的nextWaiter指向后继节点。,20230306134930b69d10589a11e885843722b1348ed25db343bc771,如果后继节点为空,则直接将前驱节点设置为尾节点。,20230306135045d13ddf0162068e8dd5d5843adeca96510367cb946,查看fullyRelease,从名字也差不多能明白该方法的作用是彻底释放锁资源。,最重要的就是release​方法,而我们上文中已经讲过了,release执行成功的话,当前线程已经释放了锁资源。,查看isOnSyncQueue,判断当前线程所在的Node​是否在同步队列中(同步队列即AQS队列)。在这里有必要给大家看一下同步队列与等待队列的关系图了。,20230306134931138ea6a0840a017050062491a86752793dbcc0926,如果当前节点的waitStatus=-2​,说明它在等待队列中,返回false​;如果当前节点有前驱节点,则证明它在AQS​队列中,但是前驱节点为空,说明它是头节点,而头节点是不参与锁竞争的,也返回false。,如果当前节点既不在等待队列中,又不是AQS​中的头结点且存在next​节点,说明它存在于AQS​中,直接返回true。,接着往下看,如果当前节点的next​为空,该节点可能是tail​节点,也可能是该节点的next还未赋值,所以需要从后往前遍历节点。,在遍历过程中,如果队列中有节点等于当前节点,返回true​;如果找到头节点也没找到,则返回false。,我们回到await的while​循环处,如果返回false,说明该节点不在同步队列中,进入循环中挂起该线程。,阿Q的理解是线程被唤醒会存在两种情况:一种是调用signal/signalAll唤醒线程;一种是通过线程中断信号,唤醒线程并抛出中断异常。,该方法的作用是判断当前线程是否发生过中断,如果未发生中断返回0​,如果发生了中断返回1​或者-1。,我们来看看transferAfterCancelledWait​方法是如果区分1和-1的,那什么情况下cas操作会成功?什么情况下又会失败呢?,当线程接收到中断信号时会被唤醒,此时node的waitStatus=-2​,所以会cas​成功,同时会将node​从等待队列转移到AQS队列中。,当线程先通过signal​唤醒后接收到中断信号,由于signal​已经将node的waitStatus​设置为-2了,所以此时会cas失败。,大家可以用下边的例子在transferAfterCancelledWait中打断点测试一下,相信就明了了。,20230306134931b92936b57446abb78166361cfb4a919a736cf3324,2023030613493273413b876f50428257f673ac1cb1ab1418fce9532,查看reportInterruptAfterWait,以上就是await的全部内容了,我们先来做个简单的总结。,如果你哪个地方存在疑问可以小窗阿Q!,signal,接下来我们再来捋一捋唤醒的过程,首先将等待队列的头结点从等待队列中取出来,20230306134932d41f20649f9f9e0b8188447fc174479d422313306,然后执行transferForSignal方法进行转移,将等待队列的头结点从等待队列转移到AQS​队列中,如果转移失败,说明该节点已被取消,直接返回false​,然后将first指向新的头结点重新进行转移。如果转移成功则根据前驱节点的状态判断是否直接唤醒当前线程。,20230306134933a1c166f343f99a284f9114acf56e30f882ce62397,怎么样?唤醒的逻辑是不是超级简单?我们也按例做个简单的总结。,从等待队列的队首开始,尝试对队首节点执行唤醒操作,如果节点已经被取消了,就尝试唤醒下一个节点。,对首节点执行唤醒操作时,首先将节点转移到同步队列,如果前驱节点的状态为取消状态或设置前驱节点的状态为唤醒状态失败,那么就立即唤醒当前节点对应的线程,否则不执行唤醒操作。,以上就是今天的全部内容了,我们下期再见。感兴趣的可以关注下公众号,也可以来技术群讨论问题呦!

© 版权声明

相关文章