当一个线程执行结束,是怎么唤醒后续线程的?一个线程cancel后是怎样出队的?上一篇主要分析了获取锁,这一篇将聚焦释放锁以解答自己的困惑。本篇还是承接《J.U.C并发框架之AQS(一):ReentrantLock获取锁》的实验用例,锁限定为非中断且非公平且独占的情形,而中断锁、非公平锁以及独占锁的研究将在后续开专篇解析。
执行lock.unlock()释放锁
当Thread-0执行到方法体的lock.unlock()语句时,预示着该线程将释放锁并运行结束。unlock()是这样一个方法:
|
|
release()如下:
|
|
首先看看tryRelease(arg)都发生了什么:
|
|
该方法releases被传入实参1,由于当前同步器的状态state = 1,所以c等于0,会执行第二个if语句的操作,但是脱离本实例来说,这里的锁是可重入的,所以也可能最后返回的free为false,不释放锁。
继续跟着实验用例分析,在tryRelease(arg)返回true后,程序向下运行,来到一个if语句,判断h是否为null以及h.waitStatus是否等于0。通过上一篇分析可知,head更改默认赋值,通常是在出现在第二个线程后,所以如果原本程序就只有一个线程在执行(这里不包括主线程以及守护线程等),那么也不存在唤醒后继节点线程的操作了,同样的,h.waitStatus如果等于0,那么要么没有后继节点,要么后继节点不需要唤醒。
unparkSuccessor方法:
|
|
可以看出当执行了unparkSuccessor方法后,如果节点的ws小于0,会先通过CAS将值设置为0,然后获得node.next并赋值给s。如果s为空,或者s的状态大于0处于cancel状态,那么就从等待队列的尾节点开始,查找第一个ws小于等于0的节点,只要这个节点存在,就调用LockSupport.unpark()将其唤醒。
至此一个正常的释放锁过程就解析完了。
cancelAcquire()
同步器内的Node持有CANCELLED字段,默认设置为1,是唯一一个大于0的waitStatus值。之前的解析多次考虑到ws大于1的情形,在AQS里,CANCELLED的设置出现在两个方法中,一个是fullyRelease(),一个是cancelAcquire(),前者主要出现在Condition的实现类,所以这里主要研究cancelAcquire(),且本文开头提到,先考虑研究独占、非中断、非公平锁的情形,也暂不考虑tryLock(),后续将用专门的篇章将这些情形补全。
cancelAcquire方法的代码不是特别长,直接放上:
|
|
如果还没创建代表当前线程的节点,自然就直接返回了,如果结点不为空,首先将它内部的thread字段置空,然后就是出队的操作了,不断向前查找第一个waitStatus不大于0的节点,设其为pred,通过pred.next获得它的下个节点,实际上这个节点waitStatus大于0应该cancel,没错,这里获取它是为了后续的CAS操作。
如果传入的节点node是尾节点,那么之前通过不断寻觅获得的第一个waitStatus不大于0的节点pred之后的节点,都应该被移除,移除方法很简单,直接将pred设为尾节点,然后将其next节点置null就行了,在源代码中,这两步采用CAS的方式实现。
如果传入的节点node不是尾节点,那就要考虑要不要唤醒node.next节点的线程。试想,如果pred正好是head节点,就将node.next节点的线程唤醒,让它尝试获取同步锁,这种情形就变成了acquireQueued方法for循环内一开始调用的情形!如果pred引用的线程为null,则也应该唤醒。除此之外,node.next节点的线程可以不需要唤醒(这里之执行unparkSuccessor(node)操作),只需要将pred的next字段指向node.next节点,而pred的ws如果不是Node.SIGNAL,还要将其设置为Node.SIGNAL。
小结
本篇主要分析了ReentrantLock释放锁的过程,还解析了cancelAcquire方法,该方法通常出现在finally语句中,AQS将tryAcquire方法的具体实现交给了衍生类,自己只是抛出一个异常:
|
|
使用cancelAcquire方法是为了解决类似这样的bug:https://bugs.openjdk.java.net/browse/JDK-6503247
参考
Java中什么样的对象才能作为gc root:在cancelAcquire方法中,处于CANCEL状态的Node对象最终会被垃圾回收,这个帖子下面的回答介绍了哪些算gc root,适合不时review;