深入解析 Java AQS (AbstractQueuedSynchronizer) 的实现原理
1. 引言
Java 并发包 (java.util.concurrent.locks
) 中的 ReentrantLock
、Semaphore
、CountDownLatch
等同步工具的核心实现都依赖于 AQS (AbstractQueuedSynchronizer)。AQS 提供了一套通用的同步框架,通过 FIFO 队列 管理线程的排队与唤醒,并支持 独占锁 和 共享锁 两种模式。
本文将深入分析 AQS 的实现原理,包括:
- AQS 的核心数据结构
- 同步队列(CLH 变体)的实现
- 线程的入队、阻塞、唤醒机制
- 公平锁与非公平锁的实现差异
- AQS 的模板方法设计
- 关键源码分析 + 流程图
2. AQS 的核心结构
AQS 的核心依赖以下几个关键组件:
volatile int state
:同步状态,不同的锁有不同的语义:ReentrantLock
:state
表示锁的重入次数(0 表示未锁定,1 表示被占用,>1 表示重入)。Semaphore
:state
表示剩余的许可数量。CountDownLatch
:state
表示剩余的计数。
- 双向同步队列(CLH 变体):存储等待获取锁的线程。
Node
节点:封装线程和等待状态。
2.1 AQS 同步队列结构
AQS 使用 双向链表 实现同步队列,每个节点是一个 Node
对象:
static final class Node {volatile int waitStatus; // 节点状态(CANCELLED, SIGNAL, CONDITION, PROPAGATE)volatile Node prev; // 前驱节点volatile Node next; // 后继节点volatile Thread thread; // 关联的线程Node nextWaiter; // 用于条件队列或共享模式
}
waitStatus
的取值:
CANCELLED (1)
:线程已取消等待(超时或中断)。SIGNAL (-1)
:当前节点的后继节点需要被唤醒。CONDITION (-2)
:节点在条件队列中(如Condition.await()
)。PROPAGATE (-3)
:共享模式下,唤醒需要传播。
head
和 tail
:
head
:虚拟头节点,不存储线程,仅用于管理队列。tail
:指向队列的最后一个节点。
同步队列示例:
head (dummy) → Node(thread1, SIGNAL) → Node(thread2, SIGNAL) → tail
3. AQS 的关键流程
3.1 获取锁(acquire)
以 ReentrantLock
为例,调用 lock()
最终会进入 AQS 的 acquire(1)
:
public final void acquire(int arg) {if (!tryAcquire(arg) && // 尝试获取锁(子类实现)acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 失败则入队selfInterrupt(); // 如果被中断,恢复中断状态
}
流程详解:
tryAcquire(arg)
(子类实现):- 非公平锁:直接尝试 CAS 修改
state
,成功则获取锁。 - 公平锁:先检查是否有前驱节点 (
hasQueuedPredecessors()
),再尝试 CAS。
- 非公平锁:直接尝试 CAS 修改
addWaiter(Node.EXCLUSIVE)
:- 将当前线程包装成
Node
,CAS 插入队尾。
- 将当前线程包装成
acquireQueued(node, arg)
:- 自旋检查前驱是否是
head
(即自己是第一个等待者)。 - 如果是,再次尝试
tryAcquire
。 - 如果不是,调用
shouldParkAfterFailedAcquire()
判断是否需要阻塞:- 前驱
waitStatus=SIGNAL
→ 可以安全阻塞(前驱释放时会唤醒自己)。 - 前驱
waitStatus=CANCELLED
→ 跳过取消的节点。
- 前驱
- 调用
LockSupport.park()
阻塞线程。
- 自旋检查前驱是否是
流程图:
graph TDA[线程调用 lock()] --> B{tryAcquire 成功?}B -->|是| C[获取锁, 直接执行]B -->|否| D[addWaiter 入队]D --> E[acquireQueued 自旋检查]E --> F{前驱是 head?}F -->|是| G[tryAcquire 尝试获取锁]G -->|成功| H[设置自己为 head, 执行]G -->|失败| I[shouldParkAfterFailedAcquire]F -->|否| II --> J[LockSupport.park 阻塞]
3.2 释放锁(release)
调用 unlock()
最终进入 release(1)
:
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)
(子类实现):- 修改
state
,如果state=0
,表示完全释放。
- 修改
unparkSuccessor(h)
:- 找到
head
后第一个未被取消的节点。 - 调用
LockSupport.unpark(node.thread)
唤醒线程。
- 找到
唤醒后的行为:
- 被唤醒的线程回到
acquireQueued
的循环,再次尝试tryAcquire
。 - 如果成功,它将成为新的
head
,并继续执行。
4. 公平锁 vs 非公平锁
4.1 非公平锁(默认)
特点:
- 新线程可以“插队”,直接尝试 CAS 获取锁,无需排队。
- 吞吐量更高,但可能导致“饥饿”。
实现 (NonfairSync
):
final void lock() {if (compareAndSetState(0, 1)) // 先尝试插队setExclusiveOwnerThread(Thread.currentThread());elseacquire(1); // 失败再走正常流程
}protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires); // 不检查队列,直接 CAS
}
4.2 公平锁
特点:
- 严格按照 FIFO 顺序获取锁,新线程必须排队。
- 避免“饥饿”,但性能较低。
实现 (FairSync
):
protected final boolean tryAcquire(int acquires) {if (!hasQueuedPredecessors() && // 检查是否有前驱节点compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}return false;
}
对比总结:
特性 | 非公平锁 | 公平锁 |
---|---|---|
获取策略 | 允许插队 | 严格 FIFO |
性能 | 更高(减少线程切换) | 较低(严格排队) |
适用场景 | 高并发、锁竞争激烈 | 需要严格顺序的场景 |
5. 总结
- AQS 的核心:
state
+ CLH 变体队列 +LockSupport
阻塞/唤醒。 - 同步队列管理:通过
Node
节点、head
/tail
指针实现线程排队。 - 公平性控制:由
tryAcquire
是否检查队列决定。 - 适用场景:
- 非公平锁:大多数高并发场景(如
ReentrantLock
默认)。 - 公平锁:需要严格顺序的场景(如任务调度)。
- 非公平锁:大多数高并发场景(如
AQS 是 Java 并发包的基石,理解它的实现有助于深入掌握 ReentrantLock
、Semaphore
、CountDownLatch
等同步工具的工作原理。