AQS 内部的等待队列被称为 "CLH 队列的变种"。要理解这个变种的精妙之处,必须先了解原版 CLH 锁是什么,以及 AQS 为了解决阻塞和共享问题对它做了什么"手术"。
一、原版 CLH 锁
CLH 锁(Craig, Landin, and Hagersten Lock)是一种自旋锁,因其发明者姓氏得名。它的核心思想是:每个线程在前驱节点的某个标志位上自旋等待,而不是在同一个全局变量上自旋。
1.1 数据结构
原版 CLH 锁是一个隐式的单向链表:
head tail
↓ ↓
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Node A │ → │ Node B │ → │ Node C │
│ (locked)│ │ (locked)│ │ (locked)│
└─────────┘ └─────────┘ └─────────┘
- 每个节点只有一个
boolean locked标志和一个next指针(指向后继)。 - 每个线程持有自己的节点和前驱节点的引用。
1.2 加锁与释放逻辑
加锁:
// 伪代码
void lock() {
Node myNode = new Node(locked = true);
Node pred = tail.getAndSet(myNode); // 原子地成为队尾,获取前驱
if (pred != null) {
while (pred.locked) { /* 自旋等待前驱释放 */ }
}
// 前驱释放了,当前线程持有锁
}
释放:
void unlock() {
myNode.locked = false; // 只需修改自己的标志,后继会在自旋中看到
}
1.3 优点与局限
| 优点 | 局限 |
|---|---|
| 每个线程在不同内存位置自旋,大幅降低缓存一致性流量(无全局竞争点) | 只能自旋,无法阻塞。意味着等待期间一直消耗 CPU |
| 出队时无需 CAS,只需修改自身标志 | 是单向链表,无法支持唤醒后继节点(因为不知道后继是谁) |
| FIFO 公平性保证 | 无法处理超时取消和中断 |
二、AQS 的 CLH 变种:关键"手术"清单
AQS 保留了 CLH 锁**"前驱驱动"的调度思想**,但对数据结构做了以下关键改造:
| 改造点 | 原版 CLH | AQS 变种 | 改造目的 |
|---|---|---|---|
| 链表方向 | 单向 (next 指针) | 双向 (prev + next) | 支持从 tail 向前遍历(处理超时取消) |
| 等待方式 | 自旋 (while (pred.locked)) | 阻塞 (LockSupport.park) + 自旋优化 | 节省 CPU 资源 |
| 节点状态 | 单一的 boolean locked | 多状态 waitStatus (SIGNAL/CANCELLED 等) | 支撑复杂的取消、传播、条件队列语义 |
| 头节点 | 真实线程节点 | 虚节点 (Dummy Node),thread == null | 简化出队逻辑,作为唤醒的"接力棒" |
| 唤醒机制 | 被动观察(自旋读取前驱标志) | 主动通知 (unparkSuccessor) | 配合阻塞模式,精准唤醒后继 |
三、AQS 变种的核心结构
3.1 双向链表
head (虚节点) tail
↓ ↓
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Node │ ←→ │ Node │ ←→ │ Node │
│ thread = null │ │ thread = 线程 A │ │ thread = 线程 B │
│ waitStatus = -1 │ │ waitStatus = -1 │ │ waitStatus = 0 │
└──────────────────┘ └──────────────────┘ └──────────────────┘
↑ prev ↑ next
prev指针:支持从tail反向遍历,处理取消节点时的安全删除。next指针:支持正向唤醒后继节点。
3.2 虚头节点 (Dummy Head)
原版 CLH 中,持有锁的线程节点就是 head,释放锁时该节点出队。
AQS 变种中,一旦线程获取锁成功,它就把自己设为一个 thread == null 的虚节点。
private void setHead(Node node) {
head = node;
node.thread = null; // 清空线程引用,变成虚节点
node.prev = null; // 断开与前驱的连接,帮助 GC
}
好处:
- 简化出队:持有锁的线程不再被队列引用,GC 可独立回收。
- 唤醒接力:虚节点作为"哨兵",它的
next指向第一个真正等待的线程,释放锁时只需unpark(head.next)。
3.3 waitStatus 状态机
原版 CLH 只有 true/false。AQS 引入了丰富的状态位,这是实现阻塞和取消的关键。
| 状态 | 含义 | 作用 |
|---|---|---|
0 | 初始状态 | 节点刚创建入队 |
SIGNAL (-1) | 唤醒责任 | 前驱释放锁时必须唤醒我 |
CANCELLED (1) | 已取消 | 节点因超时/中断失效,等待被清理 |
CONDITION (-2) | 在条件队列中 | 节点当前不在同步队列,而在 Condition 队列 |
PROPAGATE (-3) | 共享传播 | 共享模式下,唤醒需要向后链式传播 |
四、核心操作的变种实现
4.1 入队:从单向原子改为双向原子
原版 CLH(单向,只需 CAS 改 tail):
Node pred = tail.getAndSet(myNode);
AQS 变种(双向,需处理 prev 和 next 两个指针):
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t; // ① 先连 prev
if (compareAndSetTail(t, node)) { // ② CAS 发布 tail
t.next = node; // ③ 最后连 next(滞后)
return t;
}
}
}
}
关键:步骤③的滞后性导致正向遍历(next)可能暂时断裂,但反向遍历(prev)永远安全。因此 unparkSuccessor 必须从 tail 向前查找。
4.2 等待方式:从自旋改为阻塞 + 自旋混合
原版 CLH:
while (pred.locked) { /* 空转,烧 CPU */ }
AQS 变种(acquireQueued):
for (;;) {
if (p == head && tryAcquire(arg)) {
setHead(node); // 成功,设为虚头节点
return;
}
// 失败:先设置前驱为 SIGNAL,然后 park 阻塞
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
- 自旋优化:只有前驱是
head时才尝试抢锁(因为很可能马上轮到自己),避免无效自旋。 - 阻塞触发:确认前驱不是
head且已将前驱状态设为SIGNAL后,调用LockSupport.park挂起。
4.3 释放与唤醒:从被动观察到主动通知
原版 CLH:
myNode.locked = false; // 后继自己会看到
AQS 变种:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 主动唤醒后继节点
return true;
}
return false;
}
- 原版 CLH 后继一直在自旋,所以修改标志后它会立即感知。
- AQS 后继可能已阻塞,必须通过
LockSupport.unpark主动唤醒。
4.4 取消操作:从无法取消到惰性删除
原版 CLH:线程一旦排队,无法中途退出(除非一直自旋直到拿到锁)。
AQS 变种:通过 CANCELLED 状态实现安全的节点删除:
private void cancelAcquire(Node node) {
node.waitStatus = Node.CANCELLED; // 标记为取消
// 如果 node 是 tail,尝试 CAS 移除自己
// 否则,让前驱的 next 跳过自己,连接到一个有效的后继
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev; // 跳过前驱中的取消节点
Node predNext = pred.next;
node.waitStatus = Node.CANCELLED;
// 如果 node 是 tail,CAS 更新 tail 为 pred
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// 非 tail:如果前驱是 SIGNAL 且不是 head,让前驱 next 跨过自己
if (pred != head && pred.waitStatus == Node.SIGNAL) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
}
unparkSuccessor(node); // 唤醒后继,让它帮忙清理
}
}
五、共享模式下的额外变种:传播唤醒
原版 CLH 仅支持独占锁。AQS 为了支持 Semaphore 和 CountDownLatch,在共享模式中加入了传播唤醒机制。
当共享节点获取成功且发现仍有剩余资源 (tryAcquireShared 返回 > 0) 时:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared(); // 继续唤醒下一个共享节点
}
}
这是原版 CLH 完全没有的能力——一次释放可以批量唤醒多个等待者。
六、CLH 变种与原版对比总结
| 维度 | 原版 CLH 锁 | AQS CLH 变种 |
|---|---|---|
| 数据结构 | 单向隐式链表 | 双向显式链表 (prev + next) |
| 节点状态 | boolean locked | int waitStatus (SIGNAL/CANCELLED/等) |
| 等待方式 | 自旋 (CPU 忙等) | 阻塞 + 自旋优化 (park + 有限自旋) |
| 唤醒机制 | 被动观察前驱状态 | 主动 unpark 后继 |
| 取消支持 | 不支持 | 支持 (CANCELLED + 惰性删除) |
| 共享模式 | 不支持 | 支持传播唤醒 |
| 头节点 | 真实线程节点 | 虚节点 (Dummy Node) |
| 公平性 | FIFO 公平 | 支持公平/非公平(由钩子方法控制) |
一句话总结:AQS 的 CLH 变种是将原版 CLH 锁**从"自旋锁"升级为"阻塞同步框架"**的产物,它保留了 FIFO 公平性和前驱驱动的核心思想,同时引入了双向指针、多状态机、虚节点和主动唤醒,以支撑 JUC 包中各种复杂同步器的需求。