目录
- 写在前面
- 一、调用时机
- 二、源码分析
- 2.1 node为tail
- 2.2 node为中间节点
- 2.2.1 N2的取消逻辑
- 2.2.2 N3继N2之后取消
- 2.3 node是头结点的后继节点
- 2.4 并发取消的场景
- 三、思考与总结
写在前面
第一次读AQS源码时,对cancleAcquire的理解比较肤浅(停留在方法名的字面意思上了),深入了解之后才知道有很多细节。
首先需要对AQS的同步状态管理、阻塞线程的队列管理以及线程节点的几种状态机有一定了解,这里不做赘述。
JDK版本:

一、调用时机
- AQS几种获取同步状态的方式(独占式 or 共享式,超时 or 非超时,响应中断 or 不响应中断)都涉及cancleAcquire方法的调用,调用逻辑大同小异,即在线程(Node节点)阻塞的过程意外退出时,则会调用cancelAcquire()。

- 以acquireInterruptibly(独占,响应中断,非超时永久阻塞)为例:

线程阻塞的过程被中断,抛出InterruptedException,则会走到cancleAcquire的逻辑
二、源码分析
- 完整的源码:
private void cancelAcquire(Node node) {if (node == null)return;node.thread = null;Node pred = node.prev;while (pred.waitStatus > 0)node.prev = pred = pred.prev;Node predNext = pred.next;node.waitStatus = Node.CANCELLED;if (node == tail && compareAndSetTail(node, pred)) {compareAndSetNext(pred, predNext, null);} else {int ws;if (pred != head &&((ws = pred.waitStatus) == Node.SIGNAL ||(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&pred.thread != null) {Node next = node.next;if (next != null && next.waitStatus <= 0)compareAndSetNext(pred, predNext, next);} else {unparkSuccessor(node);}node.next = node; // help GC}} - 方法入参node即为待取消的节点, 整体看下来主要有两个作用:
- 修改node的状态,thread置为null,ws置为Node.CANCELLED
- node节点出队(将当前取消节点的前置非取消节点和后置非取消节点"链接"起来)
- 具体的出队逻辑分三种情况:
- node是尾结点
- node是中间节点
- node是头结点的后继结点
2.1 node为tail
- 即图中的N2节点出队,只需修改N2节点状态,修改TAIL的指向,并断开N1到N2的next指针。


- 无需断开N2到N1的prev指针,N2也会被GC回收
- 注:ws=-1为SIGNAL,ws=1为CANCLED,ws=0为初始状态,后面不赘述
2.2 node为中间节点
2.2.1 N2的取消逻辑
这里的中间节点指node既不是tail节点,也不是head的后继节点,例如下图中的N2

- N2取消的过程中,修改了N2对应Node节点的状态,并将前置非取消节点和后置非取消节点"链接"起来

- 注意这里N3对N2的prev指针并没有断开,即此时N2并没有完全出队,也不会被GC回收。这里衍生出两个问题,prev指针为什么没有断开以及是不是一直都不会断开?我们假设一个场景,N2取消之后,N3也调用了取消逻辑,借助这个场景剖析这两个问题。
2.2.2 N3继N2之后取消
N2取消之后,N3也取消的逻辑:

- 看cancleAcquire源码,有这样一段逻辑:

- 正是通过上述逻辑,取消N3的过程中,断开了N3到N2的prev指针,此时N2真正出队,等着GC回收。如果N2取消时就断开了N3到N2的prev指针,那么N3取消的时候就无法通过prev遍历到N1,所以N2取消时保留了这个prev指针。
- 后继节点N3的取消可以帮助N2完成完整的出队动作,那么如果某个节点取消之后,没有后继节点取消的场景,是如何完成出队的呢?就以N3取消的场景为例,N3的真正出队,是在N1(已经是获取了同步状态的头结点)释放同步状态唤醒其next节点(即N4)时
- 注意N4被唤醒后prev并不是head节点仍是N3,因此还会走到shouldParkAfterFailedAcquire()的逻辑中

- shouldParkAfterFailedAcquire()中,N4线程将其节点的prev指向了N1(随后尝试获取同步状态),至此N3彻底出队。

- 注意N4被唤醒后prev并不是head节点仍是N3,因此还会走到shouldParkAfterFailedAcquire()的逻辑中
总结一下:取消节点并不是调用cancleAcquire之后就彻底出队,而是保留了指向自己的prev指针,以保证后面节点一旦取消,能通过prev遍历到前面的待唤醒节点。而取消节点的真正出队有两个入口,一个是其后继节点的取消,另一个是其后继节点的唤醒。
2.3 node是头结点的后继节点
整体流程如下:

- T1线程调用cancleAcquire,由于其前驱节点已是head,会直接调用unparkSuccessor,分析源码可以看出,在unparkSuccessor中会唤醒N2节点

- N2被唤醒后,参考N4前面被唤醒的情况,会断开其到N1的prev,将prev指向head,由于前驱节点已经是head,因此可以尝试获取同步状态
- N2获取到同步状态后,更改head节点为N2,同时一行极其神奇的代码,将原head指向N1的next指针断开,至此N1才彻底出队

第一次看这个代码时不理解,因为正常情况的更改head节点,即便head的next指针不断,也不影响head本身回收,真正的关窍就是head的后继节点可能已经被取消,断掉这个next指针其实是为了后继取消节点的回收 - 这里有个细节,N1作为head的后继结点为什么要主动调用unparkSccessor唤醒N2,而不是和中间节点取消时一样将head的next指向N2等头结点释放同步状态的时候再去唤醒N2?
-
这里主要区别在于,中间节点取消时其前驱节点同样也在阻塞,而N1取消时其前驱节点是当前持有同步状态的线程,因此其状态有可能随时发生变化。
-
考虑这种场景,在N1取消的过程中,当前持有同步状态的线程(head节点)正准备释放同步状态,也走到了unparkSccessor的逻辑里,并且获取到N1的waitStatus时仍为SIGNAL(N1还没执行node.waitStatus = Node.CANCELLED;)


此时唤醒的节点是正在被取消的N1,此时N1如果不主动唤醒N2,后面的剧情极其狗血,N1完成了取消动作,以为什么也不需要做头结点就继续唤醒N2。。。最后的结果就是N2会一直阻塞,永远不会唤醒
-
结合之前中间位置节点取消的case,可以看出Doug Lea大神整体的思路就是,某个节点A取消了,如果它前面有正等待唤醒的节点B,就把唤醒后面节点的任务交给B,如果自己已经是最前面的了,就要主动唤醒!
-
2.4 并发取消的场景
假设N2和N3同时调用cancleAcquire取消



综上,T2和T3结束cancleAcquire之后的状态应为:

参考2.2.2节中的解析,T4线程被唤醒后的会执行shouldParkAfterFailedAcquire的逻辑,将状态改为:

至此,N2和N3同时完成出队
三、思考与总结
可以看出,cancleAcquire和shouldParkAfterFailedAcquire两个方法的配合十分精妙,每个方法只完成自己该完成的事情。再想一下方法名表达的字面意思,一个是“取消获取同步状态”,其负责完成当前节点的取消动作,包括节点状态的改变、将前置非取消节点和后置非取消节点“链接”起来、以及某些场景下主动唤醒后继节点;而shouldParkAfterFailedAcquire的意思是,在获取同步状态失败后是不是应该挂起线程,只有当前置节点是SIGNAL时才会返回true,基于这个条件在不满足的时候会对队列的状态进行调整,清理掉那些取消的节点。
再次膜拜Doug Lea!!!


















