Lesson 16 · 并发编程
AQS 框架与 ReentrantLock:独占锁、公平锁、Condition
面试官:你知道 Java 里的锁底层是怎么实现的吗?
在 java.util.concurrent(JUC)包中,几乎所有同步工具背后都站着同一个"幕后大佬"——AQS(AbstractQueuedSynchronizer)。这个由并发大师 Doug Lea 设计的抽象框架,是整个 JUC 锁体系的基石:
- ReentrantLock —— 可重入的独占锁,AQS 最经典的实现
- CountDownLatch —— 倒计数器,state 从 N 递减到 0 时释放所有等待线程
- Semaphore —— 信号量,state 表示可用许可数
- ReentrantReadWriteLock —— 读写锁,state 高 16 位存读锁计数、低 16 位存写锁计数
- ThreadPoolExecutor 的 Worker —— 内部也通过 AQS 实现不可重入锁
理解 AQS = 理解 JUC 的一半。核心思想极其精炼:
一个 volatile int state(同步状态) + 一条 FIFO 双向队列(CLH 变体) + 模板方法模式(子类重写 tryAcquire / tryRelease)
本文以 ReentrantLock 为主线,从 AQS 数据结构讲起,逐行拆解 lock()、unlock()、公平/非公平、Condition 全流程,最后横向串联其他 AQS 工具。
AQS 核心结构:state + CLH 队列
AQS 的全部魔力建立在两个核心成员之上。先看源码:
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer {
// 同步状态:ReentrantLock 中 0=空闲,>0=重入次数;Semaphore 中=许可数
private volatile int state;
// CLH 变体队列的头尾节点
private transient volatile Node head;
private transient volatile Node tail;
// 独占模式:子类重写
protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); }
// 共享模式:CountDownLatch / Semaphore 重写
protected int tryAcquireShared(int arg) { ... }
protected boolean tryReleaseShared(int arg) { ... }
}
Node 是 CLH 队列的节点单元,包含五个关键字段:
static final class Node {
static final int CANCELLED = 1; // 已取消(超时/中断)
static final int SIGNAL = -1; // 后继需要被 unpark
static final int CONDITION = -2; // 在 Condition 队列中
static final int PROPAGATE = -3; // 共享模式传播唤醒
volatile int waitStatus;
volatile Node prev; // 前驱(取消时回溯找有效节点)
volatile Node next; // 后继(unpark 目标)
volatile Thread thread;
Node nextWaiter; // Condition 后继 / 独占or共享标识
}
state 的修改通过 compareAndSetState() CAS 操作保证原子性,但 volatile 保证了可见性:当一个线程 CAS 修改 state 后,其他线程通过普通读(getState())能立刻看到最新值,而不需要额外加锁。没有 volatile,线程可能读到 CPU 缓存中的旧值。
CLH 队列:从自旋到 park 的进化
原始 CLH(Craig, Landin, Hagersten)是一种自旋锁队列:每个节点只包含一个 locked 字段,线程通过自旋(while(node.prev.locked))检查前驱状态。自旋的优点是响应快(无需内核态切换),但在锁竞争激烈的场景下,大量线程持续空转,CPU 利用率极高但有效吞吐为零。
AQS 做了三个关键改造,从根本上解决了自旋锁的缺陷:
- 变单向为双向:增加
prev指针,用于取消节点时向前回溯跳过已取消的前驱 - 自旋改 park/unpark:不再忙等,调用
LockSupport.park()将线程挂起(OS 层面进入 BLOCKED 状态),unpark()唤醒(恢复到 RUNNABLE)。park/unpark 底层依赖操作系统的 Mutex/Condition 或 Linux 的 futex - 增加 waitStatus:节点携带 SIGNAL、CANCELLED、CONDITION、PROPAGATE 等精细状态,精确控制唤醒时机
当线程被中断或超时取消时,需要从队列中"摘除"。如果只有 next,无法找到前驱修补链路。有了 prev,取消的节点沿 prev 向前找到第一个非取消前驱,把自己的 prev 指过去,同时更新前驱的 next,完成摘除。这就是 cancelAcquire() 的核心逻辑。
ReentrantLock.lock() 全流程拆解(NonfairSync)
ReentrantLock 默认是非公平锁(NonfairSync)。调用 lock() 时的完整流程:
final void lock() {
// ① 上来先 CAS 抢锁,不管队列有没有人 —— "插队"尝试
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // ② CAS 失败,走 AQS 标准流程
}
acquire(1) 是 AQS 的模板方法,依次调用子类 tryAcquire()、失败则入队 park:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt(); // 等待中被中断过,补上中断标记
}
NonfairSync 的 tryAcquire() 委托给 Sync.nonfairTryAcquire():
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// ③ state==0 锁空闲,CAS 抢占
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
// ④ 当前线程已持有 → 重入,state+1
int nextc = c + acquires;
setState(nextc); // 无需 CAS(只有自己能改)
return true;
}
return false; // ⑤ 被其他线程持有,获取失败
}
tryAcquire 失败后,AQS 执行 addWaiter() 入队,然后进入 acquireQueued() 自旋+park 循环:
final boolean acquireQueued(Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 前驱是 head 才有资格尝试获取锁
if (p == head && tryAcquire(arg)) {
setHead(node); // 成功,自己成为新 head
p.next = null; // 旧 head 断开,帮助 GC
failed = false;
return interrupted;
}
// 获取失败:判断是否应 park
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed) cancelAcquire(node);
}
}
检查前驱的 waitStatus。如果是 SIGNAL(-1),说明"我释放时会通知你",可以放心 park。如果前驱已取消(>0),沿 prev 跳过所有取消节点重新链接。如果前驱既不是 SIGNAL 也不是 CANCELLED,则 CAS 设为 SIGNAL,但不立即 park,再自旋一次尝试获取锁。
lock() → CAS(0→1) 成功则直接获得锁;失败 → acquire(1) → tryAcquire(CAS + 重入检查)→ 失败 → addWaiter 入队 → acquireQueued 自旋+park → 被 unpark 后重试 → 成功则将自身设为 head。
除了 lock(),ReentrantLock 还提供两个重要变体:lockInterruptibly() 在等待过程中响应中断(内部调用 acquireInterruptibly,park 后检查中断状态并抛出 InterruptedException);tryLock(timeout, unit) 支持超时等待(内部调用 tryAcquireNanos,计算 deadline,每次 park 后检查是否超时)。这两个能力是 synchronized 不具备的,也是 ReentrantLock 在实际开发中的核心优势之一。
公平锁 vs 非公平锁:一行代码的差异
通过构造函数选择模式,默认非公平:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
// FairSync 的 tryAcquire —— 比 NonfairSync 多了一个检查
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;
}
} else if (current == getExclusiveOwnerThread()) {
setState(c + acquires); // 重入
return true;
}
return false;
}
// AQS 提供的判断方法
public final boolean hasQueuedPredecessors() {
Node h = head, t = tail, s;
// 队列非空 且 我不是第一个等待者 → 有人排在我前面
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
非公平锁的 lock() 第一步就是无条件 CAS——不管队列里有没有人,上来就抢。这就是"非公平"的含义:刚来的线程可以"插队"到已排队线程前面。
| 维度 | 非公平锁(NonfairSync) | 公平锁(FairSync) |
|---|---|---|
| lock() 第一步 | 无条件 CAS(0→1),直接抢锁 | 先 hasQueuedPredecessors(),没人排队才 CAS |
| 吞吐量 | 高(减少 park/unpark 上下文切换) | 较低(严格按序,即使锁刚释放也排队) |
| 饥饿风险 | 存在(某些线程可能长期抢不到) | 不存在(FIFO 保证公平) |
| 适用场景 | 绝大多数场景(默认选择) | 资源分配需严格公平(订单处理、任务调度) |
核心原因是减少线程上下文切换。假设线程 A 刚释放锁,线程 B 正好来请求:
- 非公平:B 直接 CAS(0→1) 成功,立刻执行。省去 park → unpark → 调度 → 恢复的全部开销
- 公平:B 发现队列中有线程 C,必须入队。最终 C 被 unpark → 调度 → 获锁 → 释放 → 再 unpark B,多一次完整上下文切换
"公平和非公平的区别在于 tryAcquire 中是否调用 hasQueuedPredecessors()。非公平允许刚到的线程直接 CAS 抢锁,减少 park/unpark 开销,吞吐量更高;缺点是可能饥饿。公平严格按 FIFO,保证不饥饿但性能较低。绝大多数场景用非公平即可。"
unlock() 流程:释放锁与唤醒后继
unlock() 委托给 AQS 的 release() 模板方法:
public void unlock() { sync.release(1); }
// AQS.release() 模板方法
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒后继
return true;
}
return false;
}
// ReentrantLock.Sync.tryRelease()
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = (c == 0);
if (free) setExclusiveOwnerThread(null);
setState(c); // 无需 CAS:只有持有者会修改 state
return free;
}
unparkSuccessor() 从 tail 往前找第一个非取消节点并唤醒:
private void unparkSuccessor(Node node) {
if (node.waitStatus < 0)
compareAndSetWaitStatus(node, node.waitStatus, 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);
}
入队的 enq() 分两步:① node.prev = tail(CAS),② oldTail.next = node。两步间有时间窗口——tail 已指向新节点,但旧 tail 的 next 还没指过来。从 head.next 往后遍历可能在中间断裂。而从 tail 沿 prev 往前找是安全的,因为 prev 在 CAS 之前就已赋值。
unlock() → release(1) → tryRelease:state-1 → 若 state==0 清除 owner → unparkSuccessor:从 tail 往前找有效节点 → LockSupport.unpark() → 被唤醒线程在 acquireQueued 循环中重新竞争锁。
Condition 条件队列:两条队列的协作
newCondition() 创建的 ConditionObject 是 AQS 内部类,维护一条独立于 sync 队列的条件等待队列——AQS 最精巧的设计之一。
public final void await() throws InterruptedException {
Node node = addConditionWaiter(); // ① 加入条件队列
int savedState = fullyRelease(node); // ② 完全释放锁(保存重入次数)
while (!isOnSyncQueue(node)) {
LockSupport.park(this); // ③ park 阻塞
}
acquireQueued(node, savedState); // ④ 回到 sync 队列重新竞争锁
}
public final void signal() {
Node first = firstWaiter;
if (first != null) doSignal(first); // 唤醒条件队列第一个节点
}
private void doSignal(Node first) {
do {
if ((firstWaiter = first.nextWaiter) == null) lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) && // 转移到 sync 队列
(first = firstWaiter) != null);
}
两者都需要持锁调用、都释放锁并阻塞。关键差异:Object.wait() 只有一个 waitSet,Condition 可以创建多个实现精确唤醒(如 ArrayBlockingQueue 的 notFull/notEmpty);Condition 支持超时、不可中断等变体;底层机制不同——wait 基于 monitor,Condition 基于 AQS park/unpark。
"Condition 是 AQS 内部类 ConditionObject,维护独立条件队列。await() 释放锁、入条件队列、park;signal() 将头部节点转到 sync 队列、unpark。被唤醒后在 sync 队列重新竞争锁。多个 Condition 可实现精确唤醒,如 BlockingQueue 中的 notFull 和 notEmpty。"
除了 signal() 只唤醒一个节点,signalAll() 会将条件队列中所有节点转移到 sync 队列并逐一 unpark。典型场景如生产者-消费者模式中,生产者生产一个产品后调用 signal()(只需唤醒一个消费者),而条件变化影响所有等待者时用 signalAll()。
| 维度 | Object.wait() | Condition.await() |
|---|---|---|
| 前置条件 | 持有 synchronized 监视器锁 | 持有 ReentrantLock |
| 等待队列数 | 一个(waitSet) | 可创建多个 Condition |
| 唤醒 | notify / notifyAll | signal / signalAll |
| 超时 | 支持(wait(long ms)) | 支持(await(long, TimeUnit)) |
| 不可中断 | 不支持 | 支持(awaitUninterruptibly()) |
| 底层机制 | JVM monitor | AQS park/unpark |
AQS 家族:CountDownLatch、Semaphore、ReadWriteLock
理解了 AQS 三板斧,其他工具迎刃而解——差异仅在于state 的语义和tryAcquire/tryRelease 的实现。
Sync(int count) { setState(count); } // state = 初始计数值
protected int tryAcquireShared(int arg) {
return (getState() == 0) ? 1 : -1; // state==0 成功,否则阻塞
}
protected boolean tryReleaseShared(int arg) {
for (;;) {
int c = getState();
if (c == 0) return false;
if (compareAndSetState(c, c - 1))
return c - 1 == 0; // 减到 0 才释放所有等待线程
}
}
protected int tryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 || compareAndSetState(available, remaining))
return remaining; // <0 表示许可不够,获取失败
}
}
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState(), next = c + releases;
if (compareAndSetState(c, next)) return true;
}
}
static final int SHARED_SHIFT = 16;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; // 0x0000FFFF
// 读锁计数 = state 高 16 位(无符号右移)
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 写锁计数 = state 低 16 位(位与掩码)
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
在同一个 state 中同时记录读写锁持有数,只需一次 CAS 操作就能完成状态变更,避免维护两个独立变量的一致性问题和额外同步开销。代价是每种锁最多支持 65535 次重入。
| 工具 | state 语义 | 模式 | tryAcquire 逻辑 |
|---|---|---|---|
| ReentrantLock | 0=空闲,>0=重入次数 | 独占 | state==0 则 CAS;持有者则 state+1 |
| CountDownLatch | 倒计时计数 | 共享 | state==0 成功,否则阻塞 |
| Semaphore | 可用许可数 | 共享 | state >= acquires 则 CAS 减少 |
| ReadWriteLock | 高16位=读,低16位=写 | 独占+共享 | 写锁:无读无写则CAS;读锁:无写锁则CAS增加高位 |
除了上述四个经典工具,ThreadPoolExecutor 的 Worker 也内部继承了 AQS,实现了一个不可重入的独占锁(state 只有 0 和 1),用于保护工作线程的状态。FutureTask 的 Sync 同样基于 AQS,state 表示任务生命周期(NEW → COMPLETING → NORMAL/CANCELLED/EXCEPTIONAL)。AQS 是整个 JUC 包真正的"地基"。
总结:AQS 的设计哲学与全景图
AQS 的设计哲学归结为三个核心原则:
- 状态驱动:所有同步语义通过一个
volatile int state表达,CAS 保证原子修改 - 队列兜底:获取失败的线程统一进入 CLH 变体队列,park/unpark 管理生命周期,避免忙等
- 模板方法:AQS 定义 acquire/release 骨架,子类只需重写 tryAcquire/tryRelease 实现定制语义
模板方法模式是 AQS 设计的精髓所在。AQS 将不变的排队、park、unpark 逻辑固化在父类中(acquire、release、acquireQueued),将可变的同步语义开放给子类(tryAcquire、tryRelease)。这正是开闭原则(Open-Closed Principle)的经典体现——对扩展开放,对修改关闭。新增一个同步工具,不需要理解复杂的队列管理和线程调度细节,只需定义"什么是获取成功"和"什么是释放成功"即可。Doug Lea 用这套设计,将并发编程中最容易出错的底层细节全部封装在了 AQS 内部。
synchronized:JVM 层面实现,自动释放,代码简洁,JDK 6 后性能大幅提升(偏向锁、轻量级锁、锁升级)
ReentrantLock:API 层面实现,需手动 unlock(),但提供公平锁、可中断、超时、Condition 等高级功能
面试回答模板
Q:请介绍 AQS 的原理以及 ReentrantLock 的实现。
"AQS 是 JUC 包的核心框架,内部维护 volatile int state 和 CLH 改造的双向 FIFO 队列。AQS 采用模板方法模式,子类重写 tryAcquire/tryRelease 实现不同同步语义。
ReentrantLock 基于 AQS 实现独占锁。state=0 空闲,>0 重入次数。lock() 先 CAS(0→1),失败则 tryAcquire(含重入检查),仍失败则入 CLH 队列 park。unlock() 时 state-1,减到 0 释放并 unparkSuccessor 唤醒后继。
公平锁在 tryAcquire 中多了 hasQueuedPredecessors() 检查,保证 FIFO 顺序;非公平锁允许直接 CAS 插队,减少上下文切换,吞吐量更高。绝大多数场景用非公平即可。
Condition 通过独立条件队列实现 wait/notify。await() 释放锁入条件队列 park,signal() 转移节点到 sync 队列并 unpark。支持多个 Condition 实现精确唤醒。"
Q:公平锁和非公平锁如何选择?
"绝大多数场景用非公平锁(默认),性能更好。只有需要严格保证不饥饿或业务要求 FIFO 顺序时才用公平锁。"