AQS 到底怎么排队的?CLH 队列、state 与条件变量全链路拆解
面试常考:AQS 独占/共享模式、CLH 队列结构、state 同步状态、ConditionObject
一、从一个排队问题出发
ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock------这些并发工具底层都依赖同一个框架:AQS(AbstractQueuedSynchronizer) 。AQS 是怎么管理线程排队的?state 到底控制什么?CLH 队列怎么运作?
二、AQS 全景架构
objectivec
┌──────────────────────────────────────────────────────────────┐
│ AQS 架构 │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ volatile int state │ ← 同步状态 │
│ │ (CAS 修改) │ │
│ └────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────▼───────────────────┐ │
│ │ exclusiveOwnerThread │ ← 独占线程 │
│ │ (继承自 AbstractOwnableSynchronizer)│ │
│ └────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ CLH 等待队列 │ │
│ │ │ │
│ │ head tail │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ │ │
│ │ │Node │←▶│Node │←▶│Node │ │ │
│ │ │(哨兵)│ │Thread│ │Thread│ │ │
│ │ │ws=SIGNAL│ws=SIGNAL│ws=0 │ │ │
│ │ └──────┘ └──────┘ └──────┘ │ │
│ └────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ ConditionObject 等待队列 │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ │ │
│ │ │Node │←▶│Node │←▶│Node │ │ │
│ │ │Thread│ │Thread│ │Thread│ │ │
│ │ └──────┘ └──────┘ └──────┘ │ │
│ │ firstWaiter lastWaiter │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
三、核心字段
java
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer {
// 同步状态,volatile 保证可见性
private volatile int state;
// CLH 队列头节点(哨兵节点,不存储线程)
private transient volatile Node head;
// CLH 队列尾节点
private transient volatile Node tail;
// 独占锁持有线程(父类 AbstractOwnableSynchronizer)
// private transient Thread exclusiveOwnerThread;
}
state 的含义因实现而异
| 实现 | state 含义 | 获取方式 |
|---|---|---|
| ReentrantLock | 重入次数 (0=空闲, >0=持有) | 独占 |
| Semaphore | 剩余许可数 | 共享 |
| CountDownLatch | 剩余计数 | 共享 |
| ReentrantReadWriteLock | 高16位=读锁数, 低16位=写锁重入数 | 读写分离 |
四、Node:队列节点结构
java
static final class Node {
// 模式标记
static final Node SHARED = new Node(); // 共享模式
static final Node EXCLUSIVE = null; // 独占模式
// 等待状态
static final int CANCELLED = 1; // 节点取消
static final int SIGNAL = -1; // 后继节点需要被唤醒
static final int CONDITION = -2; // 在 Condition 队列中
static final int PROPAGATE = -3; // 共享模式传播唤醒
volatile int waitStatus; // 等待状态
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 等待的线程
Node nextWaiter; // Condition 队列的下一个节点
// 前驱节点(CLH 核心设计:每个节点监听前驱状态)
final Node predecessor() {
Node p = prev;
return (p == null) ? null : p;
}
}
为什么叫 CLH 队列?
AQS 的队列是 CLH(Craig, Landin, and Hagersten)队列的变种。原始 CLH 队列只关注前驱节点,每个节点自旋检查前驱是否释放锁 。AQS 改为阻塞式:当前驱释放锁时唤醒后继。
bash
原始 CLH: 每个节点自旋看前驱
AQS 变种: 前驱释放后 LockSupport.unpark(后继线程)
head tail
│ │
▼ ▼
┌──────┐ next ┌──────┐ next ┌──────┐
│ │───────▶│ │───────▶│ │
│ 哨兵 │ │Thread│ │Thread│
│ │◀───────│ │◀───────│ │
└──────┘ prev └──────┘ prev └──────┘
│
└── 每个节点的前驱释放锁时被 unpark 唤醒
五、独占模式:获取与释放
这是 ReentrantLock 的核心路径。
acquire:获取独占锁
java
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
三个步骤:
tryAcquire:尝试获取(子类实现),成功直接返回addWaiter:失败则加入 CLH 队列尾部acquireQueued:在队列中自旋/阻塞等待获取
addWaiter:入队
java
private Node addWaiter(Node mode) {
Node node = new Node(mode); // Thread.currentThread()
// 快速尝试在尾部添加
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 快速失败 → enq 自旋入队
enq(node);
return node;
}
private Node enq(Node node) {
for (;;) {
Node t = tail;
if (t == null) {
// 队列为空,初始化哨兵节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued:队列中等待获取
java
final boolean acquireQueued(Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor(); // 前驱节点
// 前驱是 head 且 tryAcquire 成功
if (p == head && tryAcquire(arg)) {
setHead(node); // 自己成为新的 head(哨兵)
p.next = null; // 帮助 GC
failed = false;
return interrupted;
}
// 判断是否应该阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// 根据前驱状态决定是否阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 前驱已设置 SIGNAL,可以安全阻塞
return true;
if (ws > 0) {
// 前驱已取消,跳过取消节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 前驱状态为 0 或 PROPAGATE,CAS 设为 SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 阻塞当前线程
return Thread.interrupted(); // 被唤醒后检查中断状态
}
获取锁完整流程图:
scss
┌──────────────────┐
│ acquire(arg) │
└────────┬─────────┘
│
┌──────▼───────┐
│ tryAcquire? │──成功──▶ 返回
└──────┬───────┘
失败 │
┌──────▼───────┐
│ addWaiter() │ 加入CLH队列尾部
└──────┬───────┘
│
┌──────▼───────────────┐
│ 前驱==head && │
│ tryAcquire成功? │──成功──▶ setHead, 返回
└──────┬───────────────┘
失败 │
┌──────▼───────────────┐
│ shouldParkAfter │
│ FailedAcquire()? │
└──────┬───────────────┘
│
┌─────▼──────┐
│ park阻塞 │ LockSupport.park()
│ 等待前驱 │
│ 唤醒 │
└─────┬──────┘
│ 被unpark唤醒
│
┌─────▼──────┐
│ 再次尝试 │◀──── for 循环重试
│ tryAcquire │
└────────────┘
release:释放独占锁
java
public final boolean release(int arg) {
if (tryRelease(arg)) { // 子类实现
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒后继节点
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
// 后继为空或已取消,从尾向前找第一个有效节点
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); // 唤醒后继线程
}
为什么从尾部向前查找? 因为 addWaiter 中 node.prev = pred 先于 pred.next = node 设置。从尾部向前遍历保证能找到所有有效节点。
六、共享模式
Semaphore、CountDownLatch 使用共享模式。
java
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg); // 返回值: >=0成功
if (r >= 0) {
setHeadAndPropagate(node, r); // 传播唤醒
p.next = null;
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// 共享模式特有:传播唤醒
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);
// propagate > 0 或 head 状态需要传播 → 唤醒后继共享节点
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
为什么共享模式需要传播唤醒?
java
Semaphore permits=3:
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ 哨兵 │→│ T1 │→│ T2 │→│ T3 │
└──────┘ └──────┘ └──────┘ └──────┘
线程释放1个许可 → 唤醒T1
T1获取成功,permits还剩 → 传播唤醒T2
T2获取成功,permits还剩 → 传播唤醒T3
七、ReentrantLock 的 tryAcquire 实现
java
// ReentrantLock 的 NonfairSync (非公平)
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 空闲,CAS 获取
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 重入
int nextc = c + acquires;
if (nextc < 0) throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// FairSync (公平)
final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 公平锁: 先检查队列中是否有等待线程
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
setState(nextc);
return true;
}
return false;
}
| 维度 | 非公平锁 | 公平锁 |
|---|---|---|
| 获取时 | 直接 CAS 抢 | 先检查队列 |
| 吞吐量 | 高 | 低 |
| 饥饿 | 可能 | 不会 |
| 适用场景 | 大多数场景 | 严格 FIFO |
八、ConditionObject:条件变量
java
public class ConditionObject implements Condition {
private transient Node firstWaiter; // 条件队列头
private transient Node lastWaiter; // 条件队列尾
}
Condition 维护一个独立于 CLH 队列 的单向等待队列。调用 await() 的线程从 CLH 队列移到 Condition 队列,调用 signal() 时从 Condition 移回 CLH 队列。
swift
await/signal 流程:
┌─────────────────────────────────────────────────────────┐
│ │
│ CLH同步队列 Condition等待队列 │
│ ┌──────→───→───┐ ┌───→───→───┐ │
│ │Head Node Node│ │ W1 W2 W3│ │
│ │ │ │ │ │ │ │ │ │
│ └──────←───←───┘ └───←───←───┘ │
│ │
│ await(): 当前线程从CLH移至Condition尾部, 释放锁, 阻塞 │
│ signal(): Condition队首移至CLH尾部, 等待重新获取锁 │
│ │
└─────────────────────────────────────────────────────────┘
await 核心实现
java
public final void await() throws InterruptedException {
// 1. 创建 Condition 节点加入等待队列
Node node = addConditionWaiter();
// 2. 释放锁(回到 state=0),保存之前的 state
int savedState = fullyRelease(node);
// 3. 阻塞,直到被 signal 或中断
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 4. 被 signal 后重新在 CLH 队列中竞争锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
...
}
signal 实现
java
public final void signal() {
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
// 从 Condition 队列移除
if ((firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) && // 移入 CLH 队列
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
// CAS 状态从 CONDITION 改为 0
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 加入 CLH 队列尾部
Node p = enq(node);
int ws = p.waitStatus;
// 前驱设为 SIGNAL,必要时直接唤醒
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
用 Condition 实现生产者-消费者
java
public class BoundedBuffer<T> {
private final Object[] items;
private int putIdx, takeIdx, count;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public BoundedBuffer(int capacity) {
items = new Object[capacity];
}
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await(); // 队列满,等待
items[putIdx] = item;
if (++putIdx == items.length) putIdx = 0;
count++;
notEmpty.signal(); // 通知消费者
} finally {
lock.unlock();
}
}
@SuppressWarnings("unchecked")
public T take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await(); // 队列空,等待
T item = (T) items[takeIdx];
items[takeIdx] = null;
if (++takeIdx == items.length) takeIdx = 0;
count--;
notFull.signal(); // 通知生产者
return item;
} finally {
lock.unlock();
}
}
}
九、面试高频问题速答
Q1: AQS 的 CLH 队列和原始 CLH 队列有什么区别?
原始 CLH 队列每个节点自旋检查前驱状态,不阻塞。AQS 改为阻塞式:获取失败的线程 LockSupport.park 挂起,前驱释放后 unpark 唤醒后继。AQS 还在 Node 中增加了 nextWaiter(Condition链)、waitStatus(状态标记)等字段。
Q2: AQS 为什么用 CLH 变种而不是普通队列?
CLH 易于实现取消和超时,只需修改前驱的 waitStatus;入队和出队只需 CAS 更新 tail/head,并发控制简单;每个节点只需关注前驱,天然支持取消节点的清理。
Q3: state 为什么是 volatile + CAS 而不是直接用 synchronized?
volatile 保证可见性,CAS 保证原子性,组合起来是无锁同步。避免了 synchronized 的内核态切换开销,在低竞争场景下性能更好。
Q4: 公平锁和非公平锁的区别是什么?
非公平锁 tryAcquire 直接 CAS 抢锁,不管队列中是否有等待者。公平锁先调用 hasQueuedPredecessors() 检查队列。非公平锁吞吐量高(减少线程切换),但可能导致队列中的线程长时间拿不到锁。
Q5: Condition 的 await/signal 和 Object 的 wait/notify 有什么区别?
| 维度 | Object wait/notify | Condition await/signal |
|---|---|---|
| 依赖 | synchronized | Lock |
| 队列数 | 1个 | 多个(每个 Condition 一个) |
| 精度 | notify 随机唤醒 | signal 唤醒指定条件 |
| 中断 | 不支持不响应中断 | 支持响应/不响应中断 |
| 超时 | 支持 | 支持 + 纳秒级 |
Q6: AQS 中为什么 unparkSuccessor 要从尾部向前找?
入队时 enq 方法先设置 node.prev,再 CAS 设置 tail,最后设置 pred.next。如果从头部向后找,可能遇到 pred.next 还没设置的情况导致漏掉节点。从尾部向前找 prev 是安全的,因为 prev 一定在入队时就设置好了。
十、总结
AQS 是 JUC 的基石,核心设计:
- state + CAS:无锁状态管理,不同子类赋予不同语义
- CLH 变种队列:线程安全入队,阻塞/唤醒式等待,前驱释放后唤醒后继
- 独占/共享分离:独占模式一次一个线程,共享模式支持传播唤醒
- ConditionObject:每个 Condition 独立等待队列,实现精确唤醒
理解了 AQS,ReentrantLock、Semaphore、CountDownLatch 的源码就是 "state 的不同语义 + tryAcquire/tryRelease 的不同实现" 而已。AQS 让你站在 JUC 之上看到统一的设计模式。