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 中对 next引用的注释

/**
 * 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 Node next;

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

作者:杀手小顾
链接: https://www.zhihu.com/questio...
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

你可能感兴趣的:(java,aqs,数据结构)