目录
[第 ② 步: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 → 走人
抢不到?回到开头重新抄一次。