麻瓜code头像
关注
【JUC】AQS enq() 自旋入队封面图

【JUC】AQS enq() 自旋入队

目录

只讲一件事:几个箭头怎么移动的。跟着步骤走,不用记术语。

一、先列源码

二、前置:认清楚三个箭头

三、一步步拆

第 ② 步:t = tail —— 抄快照

第 ③ 步:if (t == null) —— 判断队列空不空

分支 A:队列是空的(④ ⑤)

第 ④ 步:创建一个新节点,并设成 head

第 ⑤ 步:tail 指向这个新节点

分支 B:队列已经初始化了(⑦ ⑧ ⑨ ⑩)

第 ⑦ 步:我的前驱指向尾节点

第 ⑧ 步:判断 t 是否还指向尾节点

第 ⑨ 步:老尾节点的 next 指向我

第 ⑩ 步:结束

四、完整流程串一遍

五、我用自己话复述的版本

六、三个容易漏的点

七、一句话记住


一、先列源码

private Node enq(final Node node) {
    for (;;) {                                  // ① 死循环(自旋)
        Node t = tail;                          // ② 抄一份 tail 的指向
        if (t == null) {                        // ③ 队列是空的吗?
            if (compareAndSetHead(new Node()))  // ④ 建个新节点,设成 head
                tail = head;                    // ⑤ tail 也指向它
        } else {                                // ⑥ 队列已经初始化了
            node.prev = t;                      // ⑦ 我的前驱指向尾节点
            if (compareAndSetTail(t, node)) {   // ⑧ t 还是尾节点吗?是就把 tail 改成我
                t.next = node;                  // ⑨ 老尾节点的 next 指向我
                return t;                       // ⑩ 结束
            }
        }
    }
}

二、前置:认清楚三个箭头

head  ➤   AQS 的箭头,指向【队首】
tail  ➤   AQS 的箭头,指向【队尾】
t     ➤   临时抄写的一份 tail(快照),每轮循环重抄一次

t = tail 抄的是"地址",不是复制盒子。 两个箭头指向同一个盒子,盒子只有一个。

tail  📄 ──▶ 【盒子1】  地址 0x1A2B
t     📄 ──▶ 【盒子1】  地址 0x1A2B   ← 抄的,地址一样

t == tail 比的就是这两个地址数字一样不一样。


三、一步步拆

第 ② 步:t = tail —— 抄快照

Node t = tail;

t 就是一份快照,存放着尾节点的地址。

为什么要抄?因为 tail 是共享的,随时可能被别的线程改掉。抄一份下来,过一会儿再拿出来对暗号:

"我抄的时候 tail 指向盒子1,现在还是盒子1 吗?"


第 ③ 步:if (t == null) —— 判断队列空不空

如果 t 当前指向的尾节点还是空的(null),说明这个队列是空的。

t ➤ null    →  队列还没建
t ➤ 盒子    →  队列已经建好了

分支 A:队列是空的(④ ⑤)

第 ④ 步:创建一个新节点,并设成 head
if (compareAndSetHead(new Node()))

拆成两个动作:

动作 a:new Node()                 造一个新盒子
动作 b:compareAndSetHead(...)     用 CAS 把 head 指向这个盒子

CAS 的意思:"我以为 head 现在是 null,如果是,就让它指向这个新盒子。"

为什么要用 CAS? 因为可能好几个线程同时发现队列是空的、同时想建队列。CAS 保证只有一个能成功,其他失败的重来。

⚠️ 补充 1:这里造的是个空盒子new Node() 无参,里面没装线程)。
它叫哨兵节点,专门当"队首占位符",永远不参与排队。

  head ─┐
        ▼
     ┌────────┐
     │ 盒子1   │  ← 空的,刚诞生
     │ (哨兵)  │
     └────────┘
第 ⑤ 步:tail 指向这个新节点
tail = head;

做完 ④ 时,tail 还是 null —— 绳子只系了左端,右端还散着。

⑤ 就是把右端也系上:让 tail 也指向这个盒子

  head ─┐
        ▼
     ┌────────┐
     │ 盒子1   │
     │ (哨兵)  │
     └────────┘
        ▲
  tail ─┘

  ✅ 头结点诞生了!head 和 tail 指向同一个盒子 = 空队列建好

为什么 ⑤ 不用 CAS? 因为能走到这里的只有 ④ CAS 成功的那一个线程,没竞争。

⚠️ 补充 2:⑤ 之后没有 return!会回到 for(;;) 顶部再转一圈。
因为"建队列"和"排队"是两件事,同一个线程要转两圈才能把自己排进去。


分支 B:队列已经初始化了(⑦ ⑧ ⑨ ⑩)

否则就是:已经有了一个头结点,甚至更多,说明队列已经完成初始化。

此时 node(要插入的新节点)还在队列外面飘着。

  head ─┐                              node ─┐
        ▼                                    ▼
     ┌────────┐                         ┌────────┐
     │ 盒子1   │                         │ 盒子2   │  ← 在队外
     │ (哨兵)  │                         │ (线程T) │
     └────────┘                         └────────┘
        ▲
  tail ─┘
第 ⑦ 步:我的前驱指向尾节点
node.prev = t;

把要插入的这个节点的前驱,指向我们的尾节点。

     ┌────────┐
     │ 盒子1   │ ◀── prev ──┐
     │ (哨兵)  │            │
     └────────┘            │
        ▲                  │
  tail ─┘             ┌────────┐
                      │ 盒子2   │ ← 还在队外
                      └────────┘

⚠️ 此刻 tail 还指着盒子1,队列里没人知道盒子2 来了
只是盒子2 自己伸出 prev 钩子,单向勾住了老尾巴。

这一步安全吗? 安全。node 还在队外,只有自己看得见,改自己的箭头随便改。

第 ⑧ 步:判断 t 是否还指向尾节点
if (compareAndSetTail(t, node))

这就是拿快照对暗号

"我抄的 t 还是不是现在的 tail?"

  • → 说明没人插队,把 tail 改成指向我 ✅
  • 不是 → 说明被别人抢了,我啥也不干,回到循环顶部重抄一次 ❌

CAS 成功后:

     ┌────────┐
     │ 盒子1   │ ◀── prev ──┐
     │ (哨兵)  │            │
     └────────┘            │
                      ┌────────┐
                      │ 盒子2   │
                      └────────┘
                           ▲ ▲
                 tail ─────┘ └── node

✅ 从这一刻起盒子2 正式入队了(tail 认它了)
⚠️ 但只连了 prev,next 还没连,还是单向的
💡 t 没变过,还指着盒子1 —— ⑨ 要用它

第 ⑨ 步:老尾节点的 next 指向我
t.next = node;

这里的 t 还是之前的 tail 位置,也就是【老队尾】。我们把它 next 指向 node,他们就完全连起来了。

💡 为什么必须用 t 不能用 tail
因为 ⑧ 之后 tail 已经挪到 node 上了!写 tail.next = node 就变成 node.next = node —— 自己指自己,死循环。
t 保存了"老尾巴是谁"这个关键信息。

  head ─┐                              tail ─┐
        ▼                                    ▼
     ┌────────┐        next          ┌────────┐
     │ 盒子1   │ ────────────────▶   │ 盒子2   │
     │ (哨兵)  │ ◀────────────────   │ (线程T) │
     └────────┘        prev          └────────┘

🎉 双向链完成!

第 ⑩ 步:结束
return t;   // 返回前驱(老队尾)

💡 补充:JDK 8 的 addWaiter 里这个返回值根本没接,实际拿到的还是 node 自己。不用纠结。


四、完整流程串一遍

【起点】head ➤ null    tail ➤ null


━━━━━━━━ 第 1 轮(建队列)━━━━━━━━

② t = tail              t ➤ null
③ t == null ?           是 → 进分支 A
④ 造空盒子,CAS 设成 head
        head ─┐
              ▼
           ┌────────┐
           │ 盒子1   │ 空的(哨兵)
           └────────┘
⑤ tail = head
           ┌────────┐
           │ 盒子1   │
           └────────┘
              ▲  ▲
      head ───┘  └─── tail
   ✅ 头结点诞生
   ⚠️ 没有 return,回到 for(;;) 顶部


━━━━━━━━ 第 2 轮(排队)━━━━━━━━

② t = tail              t ➤ 盒子1  (重抄,不再是 null 了)
③ t == null ?           否 → 进分支 B
⑦ node.prev = t         盒子2.prev ➤ 盒子1   (单向,还在队外)
⑧ CAS(tail: 盒子1→盒子2) ✅ 成功,tail ➤ 盒子2
⑨ t.next = node         盒子1.next ➤ 盒子2   (双向完成)
⑩ return t              结束

五、我用自己话复述的版本

t 抄一份 tail 的地址当快照
② 快照是 null → 队列还没建 → 造个空盒子当哨兵,CAS 设为 head,再让 tail 也指过去
不返回,回到循环顶部重新抄一次快照(这次不是 null 了)
④ 走 else:先把自己的 prev 勾住老队尾(单向,私有操作,安全)
⑤ CAS 问:"我抄的快照 t 还是不是现在的 tail?" 是 → 把 tail 改成我(一锤定音)
⑥ 用 t 保存的老队尾补上 next,双向链闭合,走人


六、三个容易漏的点

说明
1. 哨兵是空盒子new Node() 没装线程,永远不排队,只当"队首占位符"。判断"轮到我了吗"= 我的 prev 是不是 head
2. ④⑤ 后不 return建队列和排队是两件事,同一个线程要转两圈
3. ⑦⑨ 顺序不能换prev 是我自己的箭头,写废了下轮覆盖就行;next改别人的字段,必须 CAS 赢了之后才敢动

七、一句话记住

读 tail → 抄快照 t → 空就建哨兵 → 不空就:勾 prev → CAS 抢 tail → 补 next → 走人
抢不到?回到开头重新抄一次。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2402_88139312/article/details/165360675

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--