AQS (AbstractQueuedSynchronizer) 核心原理详解
AQS 是 Java 并发包的基石 。它提供了一个基于 FIFO 等待队列 的框架,用于构建锁或其他同步器(如 Semaphore、CountDownLatch)。
一句话概括 :AQS 用一个
volatile int state表示资源状态,用一个 CLH 队列 存放等待资源的线程,通过 CAS 修改状态,实现了线程间的阻塞与唤醒管理。
一、 AQS 的设计目标与地位
1. 为什么要 AQS?
在没有 AQS 之前,开发者需要自己处理:
- 线程阻塞/唤醒(
park/unpark) - 线程排队(队列维护)
- 原子状态切换(CAS)
AQS 将这些共性逻辑 抽象出来,让开发者只需关注 "资源是否可用" 这一业务逻辑。
2. AQS 家族谱
AQS 是 JUC(java.util.concurrent)的骨架。
bash
java.util.concurrent.locks
├── AbstractQueuedSynchronizer (AQS)
│ ├── ReentrantLock (Sync -> NonfairSync / FairSync)
│ ├── ReentrantReadWriteLock (Sync -> ReadLock / WriteLock)
│ ├── Semaphore (Sync -> NonfairSync / FairSync)
│ └── CountDownLatch (Sync)
└── LockSupport (底层工具类,用于阻塞/唤醒线程)
二、 AQS 的两大核心
1. 同步状态(volatile int state)
state 是 AQS 的灵魂,含义由子类定义。
- ReentrantLock :
state表示锁的重入次数(0=无锁,>0=有锁)。 - Semaphore :
state表示剩余的许可证数量。 - CountDownLatch :
state表示计数器当前的值。
AQS 提供了三种原子操作来访问 state:
getState()setState()compareAndSetState()(核心,基于 CAS 实现)
2. CLH 队列(FIFO 等待队列)
AQS 内部维护了一个 双向链表队列(改良版的 Craig, Landin, and Hagersten 锁队列)。
- 作用 :存放那些获取资源失败的线程。
- 节点(Node) :队列中的每个元素都是一个
Node。thread:封装的等待线程。prev/next:前后指针。waitStatus:节点状态(极其重要,见下文)。nextWaiter:用于 Condition 条件队列。
三、 Node 节点的 waitStatus 详解
waitStatus 决定了节点的命运。
| 状态值 | 含义 | 说明 |
|---|---|---|
| CANCELLED (1) | 取消 | 线程因超时或中断被取消,不会再参与竞争。 |
| SIGNAL (-1) | 唤醒后继 | 当前节点的后继节点被(或即将被)阻塞,当前节点释放锁或被取消时必须唤醒后继节点。 |
| CONDITION (-2) | 条件等待 | 节点在 Condition 队列中,等待被 signal() 转移到同步队列。 |
| PROPAGATE (-3) | 传播 | (共享模式)确保唤醒能够向后传播(用于 ReadLock)。 |
| 0 | 初始状态 | 刚加入队列时的默认值。 |
关键逻辑:
- 只有前驱节点的
waitStatus是SIGNAL时,当前节点才能安全地park(阻塞)。 - 如果前驱节点是
CANCELLED,需要从队列中移除。
四、 AQS 的工作模式
AQS 定义了两种资源获取模式:
1. 独占模式 (Exclusive)
同一时间只有一个线程能持有资源。
- 典型实现:
ReentrantLock - 核心方法:
acquire(int arg):获取资源(独占)release(int arg):释放资源(独占)
2. 共享模式 (Shared)
多个线程可以同时持有资源。
- 典型实现:
Semaphore,CountDownLatch,ReadLock - 核心方法:
acquireShared(int arg):获取资源(共享)releaseShared(int arg):释放资源(共享)
五、 AQS 核心流程源码解析(独占模式)
1. 获取锁:acquire()
java
public final void acquire(int arg) {
if (!tryAcquire(arg) && // ① 尝试直接获取(子类实现)
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // ② 获取失败,入队并自旋
selfInterrupt();
}
流程拆解:
- tryAcquire(arg) :
- 这是 AQS 留给子类实现的钩子方法。
- 在
ReentrantLock中,它负责 CAS 修改state并检查重入。 - 如果成功,线程直接返回,不进入队列。
- addWaiter(Node.EXCLUSIVE) :
- 将当前线程包装成
Node.EXCLUSIVE节点。 - 通过 CAS 自旋,将节点追加到队列尾部。
- 将当前线程包装成
- acquireQueued(node, arg) :
- 节点在队列中"休息",等待被唤醒。
- 自旋检查 :如果前驱节点是 Head,再次尝试
tryAcquire。 - 阻塞 :如果前驱节点状态是
SIGNAL,调用LockSupport.park()挂起线程。 - 清理 :检测到线程中断或超时,将节点状态设为
CANCELLED并移除。
2. 释放锁: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(arg) :
- 钩子方法。
ReentrantLock在此将state减 1。 - 只有当
state归零时,才返回 true(完全释放)。
- 钩子方法。
- unparkSuccessor(h) :
- 找到头结点的下一个节点(即等待最久的线程)。
- 调用
LockSupport.unpark(thread)唤醒它。 - 被唤醒的线程回到
acquireQueued方法中,继续自旋尝试获取锁。
六、 Condition 的实现机制
AQS 内部类 ConditionObject 实现了 Condition 接口。
1. 两个队列
AQS 维护两个队列:
- 同步队列(CLH 队列):争抢锁的队列。
- 条件队列 (单向链表):调用
await()后等待的队列。
2. await() 流程
- 线程调用
condition.await()。 - 释放持有的锁(
state归零)。 - 将线程包装成
Node放入 条件队列 (状态为CONDITION)。 - 调用
LockSupport.park()挂起线程。
3. signal() 流程
- 线程调用
condition.signal()。 - 从 条件队列 头部取出一个节点。
- 将该节点转移到同步队列 尾部(状态改为
SIGNAL或0)。 - 当持有锁的线程调用
unlock()时,会唤醒同步队列中的这个节点。 - 该节点在同步队列中再次争夺锁。
七、 模板方法模式(核心设计思想)
AQS 采用了典型的 模板方法模式 。它实现了同步状态的管理逻辑 (排队、阻塞、唤醒),但将具体的资源获取/释放规则留给子类实现。
子类必须实现的方法(钩子):
| 方法 | 模式 | 说明 |
|---|---|---|
tryAcquire(int) |
独占 | 尝试获取资源 |
tryRelease(int) |
独占 | 尝试释放资源 |
tryAcquireShared(int) |
共享 | 尝试获取资源 |
tryReleaseShared(int) |
共享 | 尝试释放资源 |
isHeldExclusively() |
独占 | 是否被当前线程独占 |
示例:实现一个简单的互斥锁
java
class Mutex implements Lock {
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
return compareAndSetState(0, 1); // CAS 抢锁
}
@Override
protected boolean tryRelease(int arg) {
setState(0); // 释放锁
return true;
}
}
private final Sync sync = new Sync();
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
// ... 其他方法省略
}
八、 AQS 流程图(文字版)
获取锁(acquire)
bash
开始
|
v
tryAcquire() --成功--> 结束
|
失败
|
v
addWaiter() --> 入队
|
v
acquireQueued()
|
+--> 前驱是Head? --是--> tryAcquire() --成功--> 出队,设为Head
| |
| 失败
| |
| v
| park() 挂起
|
否
|
v
检查前驱状态,必要时park()
释放锁(release)
bash
开始
|
v
tryRelease() --失败--> 结束
|
成功
|
v
unparkSuccessor()
|
v
唤醒后继节点
九、 总结(面试版)
- 核心结构 :AQS =
volatile int state+ CLH 双向队列。 - 资源共享 :支持 独占 (ReentrantLock)和 共享(Semaphore/CountDownLatch)两种模式。
- 底层实现 :使用 CAS 修改状态,使用
LockSupport.park/unpark阻塞/唤醒线程。 - 排队机制:获取资源失败的线程被封装成 Node 进入队列,等待前驱节点唤醒。
- 设计模式 :模板方法模式,子类只需实现
tryAcquire/tryRelease等钩子方法。 - Condition :AQS 通过维护独立的 条件队列 ,实现了精准的线程等待/唤醒机制,比
synchronized + wait/notify更灵活。
一句话升华:
AQS 屏蔽了底层同步机制的复杂性,通过一套统一的框架,撑起了 Java 并发包半壁江山。