AQS — 抽象队列同步器
一、AQS 是什么?
知识点说明
AQS(AbstractQueuedSynchronizer) 是 JUC 的灵魂基石,几乎所有同步工具(ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock)的底层实现都依赖 AQS。
AQS = state(状态) + CLH 队列(等待队列) + CAS(原子操作)
核心思想:如果请求的共享资源空闲,则当前线程直接使用;
如果资源被占用,则将线程封装成一个 Node 节点,放入 CLH 队列等待。
核心结构
AQS 内部结构
┌────────────────────────────────────────┐
│ state(volatile int) │ ← 同步状态,0=空闲,>0=被占用
│ 子类通过 getState/setState/compareAndSetState 操作 │
├────────────────────────────────────────┤
│ CLH 双向队列 │ ← 等待队列(FIFO)
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │HEAD│ → │Node│ → │Node│ → │TAIL│ │
│ │ │ ← │ │ ← │ │ ← │ │ │
│ └────┘ └────┘ └────┘ └────┘ │
│ ↑ ↑ │
│ 前驱节点 后继节点 │
├────────────────────────────────────────┤
│ 独占模式(EXCLUSIVE) / 共享模式(SHARED) │ ← 两种获取方式
├────────────────────────────────────────┤
│ 非公平 / 公平(由子类 tryAcquire 实现) │
└────────────────────────────────────────┘
Node 节点的核心字段
| 字段 | 含义 |
|---|---|
waitStatus |
等待状态(0/CANCELLED/SIGNAL/CONDITION/PROPAGATE) |
prev |
前驱节点 |
next |
后继节点 |
thread |
当前节点持有的线程 |
nextWaiter |
共享/独占模式标记 |
waitStatus 的五种状态:
| 状态 | 值 | 含义 |
|---|---|---|
| CANCELLED | 1 | 线程已取消(超时或中断),不会再被唤醒 |
| SIGNAL | -1 | 当前节点释放锁后要唤醒后继节点 |
| CONDITION | -2 | 节点在 Condition 条件队列中等待 |
| PROPAGATE | -3 | 共享模式下,释放锁时向后传播唤醒 |
| 0 | 0 | 初始状态 |
二、独占模式(以 ReentrantLock 为例)
获取锁流程(acquire)
java
public final void acquire(int arg) {
if (!tryAcquire(arg) && // ① 子类实现:尝试获取锁(非公平/公平)
acquireQueued( // ② 获取失败,进入等待队列
addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
线程尝试获取锁
│
▼
tryAcquire() 成功?────是──→ 直接获得锁,返回
│
否
│
▼
addWaiter():将线程包装成 Node,加入 CLH 队列尾部
│
▼
acquireQueued():自旋等待
│
├── 检查前驱是否是 HEAD?
│ 是 → 再次 tryAcquire() 尝试获取
│ ├── 成功 → 设为 HEAD,返回
│ └── 失败 → 继续
│ 否 → 检查是否应该 park
│
├── shouldParkAfterFailedAcquire()
│ 前驱是 SIGNAL?→ 返回 true,准备 park
│ 前驱是 CANCELLED?→ 跳过前驱,继续往前找
│ 前驱是 0/PROPAGATE?→ CAS 设为 SIGNAL,再试一次
│
└── parkAndCheckInterrupt() → LockSupport.park() 阻塞
释放锁流程(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;
}
线程释放锁
│
▼
tryRelease() 成功(state == 0)
│
▼
unparkSuccessor():唤醒 HEAD 的后继节点(LockSupport.unpark())
│
▼
被唤醒的线程从 parkAndCheckInterrupt() 继续循环 → 尝试获取锁
三、共享模式(以 Semaphore/CountDownLatch 为例)
获取锁流程(acquireShared)
java
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) // 子类实现:返回剩余资源数,负数=获取失败
doAcquireShared(arg); // 进入等待队列
}
共享模式与独占模式的区别:
- 独占模式:一次只有一个线程获得锁
- 共享模式:多个线程可以同时获得资源(如读锁、信号量)
释放锁流程(releaseShared)
java
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 子类实现:释放资源
doReleaseShared(); // 唤醒后继节点(PROPAGATE 传播)
return true;
}
return false;
}
共享模式释放时会传播唤醒(PROPAGATE),确保多个等待线程能被依次唤醒。
四、公平锁 vs 非公平锁
非公平锁(ReentrantLock 默认)
java
// 非公平锁的 tryAcquire
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;
}
}
// 可重入逻辑...
}
// 非公平锁的 lock()
final void lock() {
// 一进来就 CAS 抢一次,抢到就用,不排队
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
公平锁
java
// 公平锁的 tryAcquire
protected 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;
}
}
// 可重入逻辑...
}
| 非公平锁 | 公平锁 | |
|---|---|---|
| 线程 arrival 时 | 直接抢锁 | 必须排队 |
| 吞吐量 | 高(减少线程切换) | 低 |
| 饥饿风险 | 有(新线程可能一直插队) | 无 |
| 适用场景 | 默认选择 | 要求公平调度 |
五、Condition 的实现原理
知识点说明
Condition 是 AQS 内部的条件等待队列。Object.wait()/notify() 基于 Monitor,而 Condition.await()/signal() 基于 AQS。
结构
AQS 同步队列(CLH): Condition 条件队列(单向链表)
┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐
│HEAD│ → │Node│ → │TAIL│ │FIRST│ → │Node│ → ...
└────┘ └────┘ └────┘ └────┘ └────┘
↑ ↑
正在等锁的线程 正在等条件的线程
工作流程
await() 流程:
① 将当前线程 Node 从同步队列移到条件队列
② 释放所有锁(保存 state 值)
③ LockSupport.park() 阻塞,等待 signal
④ 被 signal 唤醒后,重新竞争锁
⑤ 获取到锁后返回
signal() 流程:
① 将条件队列头部的 Node 移到同步队列
② 将该 Node 的 waitStatus 设为 SIGNAL
③ 唤醒该线程(unpark)
注意事项
- 一个 AQS 可以有多个 Condition(如
ReentrantLock.newCondition()可以创建多个) - await()/signal() 必须在 lock()/unlock() 之间调用(类似 wait/notify 必须在 synchronized 内)
- 多 Condition 的优势:生产者/消费者可以用不同的 Condition 分别等待,避免不必要的唤醒
六、AQS 在 JUC 中的应用
| 同步工具 | 模式 | state 含义 |
|---|---|---|
| ReentrantLock | 独占 | 0=未占用,>0=重入次数 |
| CountDownLatch | 共享 | 倒计数值 |
| Semaphore | 共享 | 剩余许可证数 |
| ReentrantReadWriteLock | 独占+共享 | 高16位=读锁数,低16位=写锁重入数 |
| ThreadPoolExecutor | — | 使用自己的 AQS(Worker 类)实现工作线程控制 |
七、面试常问
Q1:AQS 如何解决线程阻塞和唤醒的性能问题?
通过 CLH 自旋锁的变体:前驱节点释放锁时会主动唤醒后继节点。线程不会一直自旋(那样浪费 CPU),而是在尝试几次失败后进入阻塞(park),减少了上下文切换。
Q2:CLH 队列为什么是双向链表?
因为需要从后往前遍历。当唤醒后继节点时,如果后继节点被取消(CANCELLED),需要从 tail 往前找到一个有效的节点进行唤醒。
Q3:AQS 如何实现可重入?
子类在 tryAcquire 中判断:如果当前线程就是持有锁的线程,state 递增,返回 true。
Q4:共享模式和独占模式的区别?
- 独占:同一时刻只有一个线程能获取到锁(如 ReentrantLock)
- 共享:多个线程可以同时获取到锁(如 CountDownLatch/Semaphore)
- 实际中一个同步器可以同时支持两种模式(如 ReentrantReadWriteLock 的读锁共享、写锁独占)