java tail head_Java AQS unparkSuccessor 方法中for循环从tail开始而不是head的疑问?

用 API 的实现来证明自己的观点, 逻辑上是不正确的. 因为 AQS 的核心是一个 CLH 队列的变体, 整个 API 都依赖它实现. 所以是先有的 Node 类, 然后才有基于它实现的 API.

在 AQS 的 wait queue 中, 每个结点 status 都保存在它的前驱结点中 ( predecessor ). 那么为什么要这么设计? 用每个结点保存自己的 status, 然后只有当该结点是头结点并且 tryAcquire 成功时再将 head 指向下一个结点不可以么? 可以. 但是麻烦些. 因为在第一个结点入列或是结点数减少到1 时就要求既保证 head 的 CAS 设置, 又要保证 tail的. 那么如果我只将 head 视作一个逻辑头结点 ( dummy node ) 呢? 这样, 很自然的, 它存储第二个结点的状态, 第二个结点存储第三个结点的状态, 以此类推. 我就只需要控制 tail 的 CAS 设置了.

基于上述结论. 对于一个指定的结点, 我们获取它的状态最方便的就是通过一个 prev 引用获取其前驱结点, 然后获取存储在其中的状态. 所以prev 引用是务必要保证可靠的. 由于双向链表实现的队列在入列时包含两个链接的操作 ( tail.next = node; node.prev = tail ). 而 CAS 只能保证对一个变量的操作的原子性. 因此重点是保证 prev 引用的可靠, 而非 next 引用的. 因此如 @风干鸡 所提到的, 原 CLH 算法并没有 next 引用, Doug Lea 在此做出了优化, 但是不保证一个结点通过 next 引用一定能其后继结点. 可以理解为一次快速尝试. 但是由于 prev 是可靠的, 因而我们一定能通过从 tail 开始反向遍历的方式找到一个结点.

对于入列时的 tail 的 CAS 设置, 我这里在提一下. 源码:

private Node enq(final Node node) {

for (;;) {

Node t = tail;

if (t == null) { // Must initialize

if (compareAndSetHead(new Node()))

tail = head;

} else {

node.prev = t; // ①

if (compareAndSetTail(t, node)) { // ②

t.next = node; // ③

return t;

}

}

}

}

① 处将新结点 node 的 prev 引用指向当前的 t, 即 tail 结点. 然而, 由于 ①, ② 这两行代码的合在一起并非原子性的, 所以很有可能在设置 tail 时存在着竞争, 也即 tail 被其它线程更新过了. 所以要自旋操作, 即在死循环中操作, 直到成功为止. 自旋地 CAS volatile 变量是很经典的用法. 如果设置成功了, 那么 从 node.prev 执行完毕到正在用 CAS 设置 tail 时, tail 变量是没有被修改的, 所以如果 CAS成功, 那么 node.prev = t 一定是指向上一个 tail 的. 同样的, ②, ③ 合在一起也并非原子操作, 更重要的是,next field 的设置发生在 CAS 操作之后, 所以可能会存在 tail 已经更新, 但是 last tail 的 next field 还未设置完毕, 即它的 lastTail.next 为 null 这种情况. 因此如果此时访问该结点的 next 引用可能就会得到它在队尾, 不存在后继结点的"错觉". 而我们总是能够通过从 tail 开始反向查找, 借助可靠的 prev 引用来定位到指定的结点. 简单总结一下,prev 引用的设置发生在 CAS之前, 因此如果 CAS 设置 tail 成功, 那么 prev 一定是正确地指向 last tail, 而 next 引用的设置发生在其后, 因而会存在一个 tail 更新成功, 但是 last tail 的 next 引用还未设置的尴尬时期. 所以我们说 prev 是可靠的, 而 next 有时会为 null, 但并不一定真的就没有后继结点.

附上 JDK 8 中 AQS.Node 中对 prev 引用的注释/**

* Link to the successor node that the current node/thread unparks upon release. Assigned during enqueuing, adjusted when bypassing cancelled predecessors, and nulled out (for sake of GC) when dequeued. The enq operation does not

assign next field of a predecessor until after attachment, so seeing a null next field does not necessarily mean that node is at end of queue. However, if a next field appears to be null, we can scan prev's from the tail to double-check. The next field of cancelled nodes is set to point to the node itself instead of null, to make life easier for isOnSyncQueue.

*/

volatile Nodenext;

综上所述, 因为避免对 head 和 tail 同时原子性的更新, 使 head 总是一个 dummy 结点, 很自然的 结点的 status 总是存储在其前驱结点中. 所以为了方便访问前驱结点, prev 引用就一定要保证是可靠的. 而 CAS 只能保证一个变量的操作的原子性, 因此 next 引用不需要是可靠的, 存在就是为了方便快速获取后继结点, 然而由于不可靠, 所以不保证能获取成功. 所以从 tail 开始反向遍历是一定能查找到指定结点的.

才疏学浅, 难免错漏. 欢迎诸君不吝赐教.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值