0%

AQS-cancelAcquire 方法源码解析

前言

AQS 的 cancelAcquire 方法值得花一篇文章来写,因为虽然代码不多,但是涉及到的情况不少。

cancelAcquire

在 AQS 源码中该方法共被 6 个地方调用(3 个独占锁,3 个共享锁):
acquireQueued / doAcquireInterruptibly / doAcquireNanos
doAcquireShared / doAcquireSharedInterruptibly / doAcquireSharedNanos

该方法目的为:获取过程抛出异常后,取消正在进行的获取尝试。

不考虑 Condition 的前提下,该方法是唯一会将 sync queue 中 Node 的 waitStatus 置为 CANCELLED 的方法。

穷举所有 case

从该方法最后部分 if (node == tail && ...if (pred != head && ... 两句可以看出,该方法进行操作的三个不同的大前提条件为:

  1. node 是 tail
  2. node 不是 tail && pred 不是 head
  3. node 不是 tail && pred 是 head

我们先不考虑这些 case 如何达成,本文我们暂时只关注:穷举出所有 case 并看每种 case 的最终结果

注:下文中 normalNode 代表 waitStatus <= 0 的节点;cancelNode 代表 waitStatus 为 CANCELLED 的节点。

  1. node 是 tail + pred 是 head

    head <=> [node]
    head <=> cancelNode <=> [node]

  2. node 是 tail + pred 不是 head

    head <=> normalNode <=> [node]
    head <=> normalNode <=> cancelNode <=> [node]

  3. node 不是 tail && pred 不是 head + node 后为 normalNode

    head <=> normalNode <=> [node] <=> normalNode
    head <=> normalNode <=> cancelNode <=> [node] <=> normalNode

  4. node 不是 tail && pred 不是 head + node 后为 cancelNode

    head <=> normalNode <=> [node] <=> cancelNode
    head <=> normalNode <=> cancelNode <=> [node] <=> cancelNode

  5. node 不是 tail && pred 是 head + node 后为 normalNode

    head <=> [node] <=> normalNode
    head <=> cancelNode <=> [node] <=> normalNode

  6. node 不是 tail && pred 是 head + node 后为 cancelNode

    head <=> [node] <=> cancelNode
    head <=> cancelNode <=> [node] <=> cancelNode

(在下文中会看到,其实 5 和 6 是可以合并的)

node 是 tail + pred 是 head

case1.1

head <=> [node]

case1.1

case1.2

head <=> cancelNode <=> [node]

case1.2

何时清除 node

当 head 节点变更时,即前置线程释放锁,sync queue 中第一个排队线程获取锁后调用了 setHead 方法和 p.next = null 后。

node 是 tail + pred 不是 head

case2.1

head <=> normalNode <=> [node]

case2.1

case2.2

head <=> normalNode <=> cancelNode <=> [node]

case2.2

何时清除 node

当排队线程陆续获取锁直到 pred 变为 head,然后(同 case1)当 head 节点变更时,即前置线程释放锁,sync queue 中第一个排队线程获取锁后调用了 setHead 方法和 p.next = null 后。

node 不是 tail && pred 不是 head + node 后为 normalNode

case3.1

head <=> normalNode <=> [node] <=> normalNode

case3.1

case3.2

head <=> normalNode <=> cancelNode <=> [node] <=> normalNode

case3.2

何时清除 node

当别的线程调用 shouldParkAfterFailedAcquirecancelAcquire 方法时,会调整其遍历节点的指针,(同 case2)当排队线程陆续获取锁直到 node 后的 normalNode 变为 head,然后当 head 节点变更时,即前置线程释放锁,sync queue 中第一个排队线程获取锁后调用了 setHead 方法和 p.next = null 后。

node 不是 tail && pred 不是 head + node 后为 cancelNode

case4.1

head <=> normalNode <=> [node] <=> cancelNode

case4.1

case4.2

head <=> normalNode <=> cancelNode <=> [node] <=> cancelNode

case4.2

何时清除 node

当 node 后面的 cancelNode 后面有了 normalNode,(同 case3)当别的线程调用 shouldParkAfterFailedAcquirecancelAcquire 方法时,会调整其遍历节点的指针,当排队线程陆续获取锁直到 node 后的 normalNode 变为 head,然后当 head 节点变更时,即前置线程释放锁,sync queue 中第一个排队线程获取锁后调用了 setHead 方法和 p.next = null 后。

case5 + case6

case5:node 不是 tail && pred 是 head + node 后为 normalNode
case6:node 不是 tail && pred 是 head + node 后为 cancelNode

可以发现,其实该方法的操作不会涉及到 node 后所跟的节点,因此,无论 node 后跟的是 normalNode 或 cancelNode,结果其实都是一样的。

case5.1 + case6.1

head <=> [node] <=> normalNode
head <=> [node] <=> cancelNode

case5.1

case5.2 + case6.2

head <=> cancelNode <=> [node] <=> normalNode
head <=> cancelNode <=> [node] <=> cancelNode

case5.2

何时清除 node

  1. 当 node 后是 normalNode 时,会唤醒线程去获取锁,获取到锁后该节点变为头节点,注意此时 node 其实还没有彻底清除掉,还需要此后(同 case1)当 head 节点变更时,即前置线程释放锁,sync queue 中第一个排队线程获取锁后调用了 setHead 方法后
  2. 当 node 后是 cancelNode 时,啥也不会做,因为唤不醒该线程,只能等(同 case4)当 node 后面的 cancelNode 后面有了 normalNode,当别的线程调用 shouldParkAfterFailedAcquirecancelAcquire 方法时,会调整其遍历节点的指针,当排队线程陆续获取锁直到 node 后的 normalNode 变为 head,然后当 head 节点变更时,即前置线程释放锁,sync queue 中第一个排队线程获取锁后调用了 setHead 方法和 p.next = null

小结

该方法无论什么条件,都会做的事是:

  1. pred 指向从 node 往前遍历的第一个状态不为 CANCELLED 的节点
  2. node.prev 指向 pred
  3. node 的 waitStatus 置为 CANCELLED

此后,当条件为:

  1. node 是 tail:将 tail 指向 pred;将 pred.next 指向 null
  2. node 不是 tail && pred 不是 head:将 pred 的 waitStatus 置为 SIGNAL;如果 node.next 的 waitStatus 不为 CANCELLED,将 pred.next 指向 node.next
  3. node 不是 tail && pred 是 head:调用 unparkSuccessor 方法唤醒 node 的后继节点

可以发现该方法很多结果最终都是修改 next 指针而保留 prev 指针,我认为这是因为考虑到还有很多方法都采用从 tail 往前遍历,而遍历时会跳过 waitStatus 为 CANCELLED 的节点。因此在这里只要修改节点的 waitStatus 以及 next 指针就足够了,早晚都会有别的方法会通过遍历 / setHead / p.next = null 去将 waitStatus 为 CANCELLED 的节点从 sync queue 中完全断开!


欢迎关注我的其它发布渠道