AQS unparkSuccessor 方法中for循环从tail开始而不是head的解释

郝思文
• 阅读 2265

用 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...
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
VBox 启动虚拟机失败
在Vbox(5.0.8版本)启动Ubuntu的虚拟机时,遇到错误信息:NtCreateFile(\\Device\\VBoxDrvStub)failed:0xc000000034STATUS\_OBJECT\_NAME\_NOT\_FOUND(0retries) (rc101)Makesurethekern
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql中时间比较的实现
MySql中时间比较的实现unix\_timestamp()unix\_timestamp函数可以接受一个参数,也可以不使用参数。它的返回值是一个无符号的整数。不使用参数,它返回自1970年1月1日0时0分0秒到现在所经过的秒数,如果使用参数,参数的类型为时间类型或者时间类型的字符串表示,则是从1970010100:00:0
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这