【JUC】AQS enq() 自旋入队

目录

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

一、先列源码

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

三、一步步拆

[第 ② 步:t = tail ------ 抄快照](#第 ② 步:t = tail —— 抄快照)

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

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

[第 ④ 步:创建一个新节点,并设成 head](#第 ④ 步:创建一个新节点,并设成 head)

[第 ⑤ 步:tail 指向这个新节点](#第 ⑤ 步:tail 指向这个新节点)

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

[第 ⑦ 步:我的前驱指向尾节点](#第 ⑦ 步:我的前驱指向尾节点)

[第 ⑧ 步:判断 t 是否还指向尾节点](#第 ⑧ 步:判断 t 是否还指向尾节点)

[第 ⑨ 步:老尾节点的 next 指向我](#第 ⑨ 步:老尾节点的 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 → 走人
抢不到?回到开头重新抄一次。
相关推荐
Wang's Blog1 小时前
Java框架快速入门: Spring Security+OAuth2之核心角色与授权流程
java·spring·github
hai_android2 小时前
Kotlin Flow combine 源码剖析:一个 Channel 如何优雅合并多条流
android·java·kotlin
caoerzhong2 小时前
JeeWMS 开源仓库管理系统权限与域验证解析:Java WMS 如何把数据权限管到仓、货主与人
java·python·开源·vue
君顾12 小时前
西安 24 小时自助健身房系统开发实战指南
java·开发语言·健身房
用户094248568032 小时前
第9章:OpenJDK堆分代与 Serial/Parallel GC 基础
java·jvm
Wang's Blog2 小时前
Java框架快速入门: Spring Security+OAuth2之RBAC与角色分层实践
java·网络·spring
caoerzhong2 小时前
JeeWMS 开源仓库管理系统多租户架构解析:一套 Java WMS 如何同时服务多个货主与多个仓库
java·架构·开源·vue
sunshine22 girl2 小时前
Java学习四 面向对象原理
java
干到60岁退休的码农2 小时前
15.创建获取用户接口与JWT认证器
java·spring boot·mybatis