1. AQS 源码面试三板斧:从"用过"到"懂过"的关键跨越
在 Java 并发编程面试中,AbstractQueuedSynchronizer(AQS)就像一道分水岭。我见过太多候选人能背出"AQS 是构建锁和同步器的基础框架"这样的定义,却在面试官追问三行源码时突然语塞。今天我们就直击要害,拆解面试官最常考察的三段 AQS 源码,让你从"听说过"跃升到"真懂过"。
1.1 state 字段:并发世界的原子裁判
java复制public abstract class AbstractQueuedSynchronizer {
private volatile int state;
protected final int getState() {
return state;
}
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
}
这段代码看似简单,却藏着三个面试必考点:
-
volatile 的双重作用:不仅保证可见性,更重要的是防止指令重排序。在 AQS 中,state 的修改必须对其他线程立即可见,否则会导致锁状态不一致。
-
CAS 操作的深层含义:compareAndSetState 不是简单的 setter,而是整个 AQS 的基石。它实现了无锁化的状态变更,这才是 AQS 高性能的关键。
-
state 的哲学意义:这个 int 变量不是锁,而是"资格凭证"。比如在 ReentrantLock 中,state=0 表示空闲,state=1 表示被占用,state>1 表示重入次数。
面试陷阱:当面试官问"state 代表什么?"时,平庸的回答是"表示锁的状态",而高手会说:"state 是同步状态的抽象表示,具体语义由子类定义。比如在 Semaphore 中它表示剩余许可数,在 CountDownLatch 中表示剩余计数。"
1.2 acquire 方法:一次机会的公平博弈
java复制public final void acquire(int arg) {
if (!tryAcquire(arg)) {
Node node = addWaiter(Node.EXCLUSIVE);
acquireQueued(node, arg);
}
}
这段代码揭示了 AQS 最精妙的设计哲学:
-
tryAcquire 的战术地位:这是模板方法模式的应用,子类必须实现自己的获取逻辑。但关键在于它只有一次尝试机会,失败立即进入排队流程。
-
非公平锁的本质:新线程总是先尝试获取锁(tryAcquire),失败才排队。这意味着新线程有机会"插队",这就是非公平锁的实现原理。
-
addWaiter 的细节:Node.EXCLUSIVE 表示独占模式,对应的还有 SHARED 模式。这里创建的 Node 会包含当前线程信息,并通过 CAS 操作快速入队。
避坑指南:很多面试者说不清为什么要有 tryAcquire 这步直接尝试。其实这是为了减少线程切换开销——如果锁刚好可用,就不必经历昂贵的挂起/唤醒过程。
1.3 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;
}
release 方法的关键点常被低估:
-
tryRelease 的契约:子类必须确保只有锁的持有者才能成功释放。返回 true 只表示释放操作合法,不保证立即唤醒后继节点。
-
唤醒时机的精妙控制:只有当 head 的 waitStatus≠0 时才唤醒后继者。这个状态记录着后继节点是否需要被唤醒(SIGNAL=-1)。
-
unparkSuccessor 的健壮性:这个方法会从尾部向前遍历找到第一个未取消的节点,避免并发修改导致的问题。
血泪教训:我曾见过一个自定义同步器 bug,tryRelease 返回 true 但忘记设置 head 的状态,导致后续线程永远阻塞。面试官特别喜欢问:"如果忘记唤醒会怎样?"现在你知道答案了。
2. AQS 设计哲学深度解析
2.1 状态机模型:AQS 的本质
AQS 本质上是一个状态机,state 是状态变量,acquire/release 是状态转换条件。理解这一点,就能明白为什么 AQS 能衍生出各种同步工具:
- 互斥锁:state∈
- 可重入锁:state∈N
- 信号量:state∈[0,N]
- 闭锁:state∈{0,1}(但语义不同)
2.2 队列管理:CLH 变体的智慧
AQS 的等待队列是 CLH 锁的变体,但有三处关键改进:
- 显式维护前驱指针:用于处理取消和超时
- 状态字段传播:通过 waitStatus 实现高效的唤醒传递
- 虚拟头节点:简化边界条件处理
java复制// 典型的入队逻辑
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node); // 完整入队逻辑
return node;
}
2.3 模板方法模式:扩展点的艺术
AQS 定义了五个可重写的方法:
- tryAcquire:尝试获取独占锁
- tryRelease:尝试释放独占锁
- tryAcquireShared:尝试获取共享锁
- tryReleaseShared:尝试释放共享锁
- isHeldExclusively:是否被当前线程独占
设计启示:这种设计让 AQS 既能支持 ReentrantLock 这样的独占锁,也能支持 Semaphore 这样的共享锁,体现了"开闭原则"。
3. 面试实战:如何优雅应对 AQS 问题
3.1 高频问题清单与应对策略
| 问题类型 | 常见问题 | 高分回答要点 |
|---|---|---|
| 基础原理 | AQS 的工作原理是什么? | 从 state、队列、模板方法三个维度回答 |
| 源码细节 | acquire 和 release 的流程是怎样的? | 结合 CAS、队列操作、唤醒机制说明 |
| 设计思想 | 为什么 AQS 要这样设计? | 谈性能(减少阻塞)、扩展性(模板方法)、健壮性(状态校验) |
| 实战应用 | 如何基于 AQS 实现自定义锁? | 举例说明 tryAcquire/tryRelease 的实现要点 |
3.2 避坑指南:AQS 面试常见误区
-
过度关注队列算法:除非面试官明确要求,否则不必深入 CLH 队列的实现细节,重点说清楚它的作用即可。
-
混淆抽象与实现:要区分 AQS 提供的框架能力和具体同步器(如 ReentrantLock)的实现逻辑。
-
忽视可重入性:在讨论 state 时,要说明它在可重入锁中的特殊含义。
-
低估取消机制:AQS 的节点取消(CANCELLED 状态)是个复杂但常被忽视的点。
3.3 加分项:展示深度理解的技巧
当你想展现真正的 AQS 功底时,可以主动提及:
- 自旋优化:acquireQueued 方法中有限次数的自旋尝试
- 中断处理:acquireInterruptibly 与普通 acquire 的区别
- 超时控制:tryAcquireNanos 如何实现精确超时
- 条件变量:ConditionObject 与 AQS 的协同工作原理
4. 从源码看 AQS 的性能考量
4.1 减少系统调用的设计哲学
AQS 在性能优化上有几个关键决策:
- 快速路径优先:先尝试直接获取,失败才入队
- 延迟初始化:队列头节点懒加载
- CAS 失败重试:enq 方法中的循环 CAS 保证最终成功
- 唤醒传播:只唤醒必要的后继节点
4.2 内存可见性的精妙控制
AQS 对内存屏障的使用非常考究:
java复制// 典型的释放模式
setState(newState); // volatile 写
if (needWakeup) {
unparkSuccessor(h); // 可能触发 volatile 读
}
这种操作顺序确保了 happens-before 关系,既保证正确性,又避免不必要的内存屏障开销。
4.3 竞争激烈时的降级策略
当系统检测到高竞争时,AQS 相关实现类(如 ReentrantLock)会采取策略:
- 增加自旋次数:在挂起前多尝试几次
- 队列压缩:清除已取消的节点
- 超时优化:更精确的纳秒级超时控制
5. 手写 AQS 子类的实战要点
5.1 实现不可重入互斥锁
java复制class Mutex extends AbstractQueuedSynchronizer {
protected boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int releases) {
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0); // 必须先清 owner 再改 state
return true;
}
}
关键点:
- CAS 确保原子获取
- 释放时先清 owner 再改 state(避免指令重排序问题)
- 不可重入所以不检查当前线程
5.2 实现简单的信号量
java复制class SemaphoreOnAQS extends AbstractQueuedSynchronizer {
SemaphoreOnAQS(int permits) {
setState(permits);
}
protected int tryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 || compareAndSetState(available, remaining)) {
return remaining;
}
}
}
protected boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next)) {
return true;
}
}
}
}
注意:
- 使用自旋 CAS 处理并发更新
- 返回值表示剩余许可数
- 处理整数溢出边界情况
6. AQS 在 JDK 中的应用实例
6.1 ReentrantLock 的公平与非公平实现
公平锁:
java复制protected final boolean tryAcquire(int acquires) {
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
// 检查重入情况...
}
非公平锁:
java复制final boolean nonfairTryAcquire(int acquires) {
if (compareAndSetState(0, acquires)) { // 直接尝试获取
setExclusiveOwnerThread(current);
return true;
}
// 检查重入情况...
}
关键区别就在 hasQueuedPredecessors() 检查!
6.2 CountDownLatch 的一次性特性
java复制protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0) return false;
int nextc = c-1;
if (compareAndSetState(c, nextc)) {
return nextc == 0;
}
}
}
注意:
- 计数到零后不能再重置
- 释放操作会传播到所有等待线程
- 使用共享模式实现批量唤醒
7. AQS 相关的进阶面试题
7.1 为什么 AQS 要同时维护 prev 和 next 指针?
双向链表的主要考虑:
- 便于处理取消节点(从中间移除)
- 支持条件队列的转移
- 但 next 指针只是优化,非必须(见源码注释)
7.2 AQS 如何处理线程中断?
两种模式:
- 不可中断模式(acquire):即使被中断也继续等待,获取锁后再补上中断状态
- 可中断模式(acquireInterruptibly):立即抛出 InterruptedException
7.3 AQS 与 Synchronized 的性能对比
关键差异点:
- AQS 提供更灵活的阻塞策略(可中断、可超时)
- Synchronized 有 JVM 内置优化(偏向锁、轻量级锁)
- 高竞争时 Synchronized 可能更好,低竞争时 AQS 更灵活
8. 从 AQS 看 Java 并发设计模式
8.1 状态模式的应用
AQS 将同步状态(state)与状态行为(acquire/release)分离,不同子类可以赋予 state 不同语义,这是典型的状态模式应用。
8.2 模板方法模式的典范
AQS 定义了算法骨架(排队、唤醒等),将关键步骤延迟到子类实现(tryAcquire等),这是模板方法模式的教科书级实现。
8.3 避免惊群效应的策略
AQS 的 unparkSuccessor 只唤醒一个线程,避免了不必要的线程唤醒(惊群效应),这种设计在操作系统和数据库中也常见。
9. 常见问题排查与调试技巧
9.1 死锁诊断
当怀疑 AQS 相关死锁时:
- 用 jstack 查看线程栈
- 关注 "AbstractQueuedSynchronizer$Node" 的线程状态
- 检查锁的持有者和等待链
9.2 性能问题定位
AQS 相关性能问题通常表现为:
- 高 CPU(自旋过多)
- 上下文切换频繁(线程挂起/唤醒)
- 可通过 JFR 监控 park/unpark 事件
9.3 调试技巧
调试 AQS 的实用方法:
- 重写 tryAcquire 加入日志
- 使用 ThreadMXBean 监控锁竞争
- 可视化工具观察等待队列
10. 总结:AQS 面试的三重境界
- 知道是什么:能说出 AQS 的基本原理和作用
- 理解为什么:明白设计选择和取舍考量
- 掌握怎么做:能基于 AQS 实现自定义同步器
当你能够从这三个层面回答 AQS 相关问题,面试官看到的不仅是一个会用 Java 并发工具的开发者,更是一个理解并发编程本质的工程师。记住,AQS 面试不是考察你能背多少实现细节,而是检验你是否真正理解了 Doug Lea 大师的设计哲学——用最简单的方式解决最复杂的并发问题。