AQS 原理
候选人小唐在面试拼多多 P7 时,面试官问道:
"你能手写一个简单的 ReentrantLock 吗?"
小唐说:"可以用 synchronized..."面试官说:"我们聊聊 AQS 吧。AQS 的核心结构是什么?它是怎么管理等待队列的?"
小唐彻底答不上来了...
一、核心问题:AQS 原理 🔴
1.1 问题拆解
第一层:定位(是什么?)
"AQS 在 JUC 中的位置是什么?哪些类依赖 AQS?"
考察点:AbstractQueuedSynchronizer 的作用
第二层:核心结构(有什么?)
"AQS 的核心数据结构和状态管理是什么?"
考察点:volatile state、CLH 队列、Node
第三层:模板方法(怎么用?)
"AQS 为什么用模板方法模式?tryAcquire 和 tryRelease 是什么角色?"
考察点:设计模式、模板方法
第四层:独占与共享(两种模式)
"AQS 的独占模式和共享模式有什么区别?分别在哪些场景使用?"
考察点:CountDownLatch、CyclicBarrier、Semaphore
1.2 ❌ 错误示范
候选人原话 A:"AQS 是一个队列,用来管理线程。"
问题诊断:AQS 不只是一个队列。它是一个状态管理 + 等待队列的框架。CLH 队列只是 AQS 的一部分,核心是 volatile int state 的状态管理。
候选人原话 B:"AQS 的 acquire() 就是获取锁,release() 就是释放锁。"
问题诊断:AQS 有两种模式——独占(acquire/release)和共享(acquireShared/releaseShared)。CountDownLatch、CyclicBarrier、Semaphore 使用共享模式,ReentrantLock、ReentrantReadWriteLock.WriteLock 使用独占模式。
1.3 标准回答
P5 级别:AQS 定位与核心结构
AQS 的定位:
AbstractQueuedSynchronizer(队列同步器)是 JUC 包中几乎所有锁和同步器的底层实现,包括:
ReentrantLock(可重入独占锁)
ReentrantReadWriteLock(读写锁)
Semaphore(信号量)
CountDownLatch(倒计时门栓)
CyclicBarrier(循环栅栏)
核心结构:
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer {
// 1. 状态:volatile int state
// 0 = 未占用,正数 = 已占用(可重入计数)
private volatile int state;
// 2. CLH 队列(等待队列)
// 双向链表,头尾指针,存储等待线程
private transient volatile Node head;
private transient volatile Node tail;
// 3. Node(等待节点)
static final class Node {
volatile Node prev;
volatile Node next;
volatile Thread thread; // 等待的线程
int waitStatus; // SIGNAL/CANCELLED/CONDITION/SHARED/PROPAGATE
boolean isShared; // 共享模式标记
}
}
AQS 的设计哲学:
AQS 使用模板方法模式——框架定义获取/释放的骨架算法,子类实现具体的获取策略(tryAcquire)和释放策略(tryRelease)。
P6 级别:acquire 与 release 流程
独占模式获取锁(acquire):
public final void acquire(int arg) {
if (!tryAcquire(arg)) { // 子类实现:尝试获取锁
if (acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt(); // 线程中断标志设置
}
}
}
// 1. addWaiter:将当前线程封装为 Node,加入 CLH 队列尾部
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)) { // CAS 快速插入
pred.next = node;
return node;
}
}
enq(node); // CAS 失败,进入 enq 自旋插入
return node;
}
// 2. acquireQueued:在队列中自旋等待获取锁
final boolean acquireQueued(Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
Node p = node.predecessor(); // 前驱节点
if (p == head && tryAcquire(arg)) { // 前驱是 head 且获取成功
setHead(node); // 出队,当前节点变为 head
p.next = null; // 帮助 GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node)) { // 阻塞前调整
if (parkAndCheckInterrupt()) { // 挂起线程
interrupted = true;
}
}
}
} finally {
if (failed) cancelAcquire(node);
}
}
CLH 队列的精髓:
CLH 队列(Craig, Landin, and Hagersten)是自旋锁的变体,但这里的"自旋"不在 CPU 忙等,而是在 LockSupport.park() 挂起。线程在 park 后不消耗 CPU,通过前驱节点的 unpark 唤醒。
独占模式释放锁(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;
}
// 唤醒后继节点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0) {
compareAndSetWaitStatus(node, ws, 0); // 清理 waitStatus
}
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); // 唤醒线程
}
P7 级别:共享模式与 ConditionObject
共享模式(Semaphore、CountDownLatch):
共享模式下,多个线程可以同时获取锁:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) { // 返回剩余资源数,负数表示获取失败
doAcquireShared(arg); // 加入共享队列并阻塞
}
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 原子释放资源
doReleaseShared(); // 唤醒后继节点(可能多个)
return true;
}
return false;
}
CountDownLatch 的实现:
// Sync(继承 AQS)
protected int tryAcquireShared(int acquires) {
return getState() == 0 ? 1 : -1; // state=0 表示 latch 已打开
}
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0) return false; // 已打开,不能再 release
int nextc = c - 1;
if (compareAndSetState(c, nextc)) { // CAS 递减
return nextc == 0; // 返回 true 时唤醒所有等待线程
}
}
}
ConditionObject(条件队列):
每个 Condition 对应一个独立的等待队列,与 AQS 的 CLH 队列分离:
- await():释放已持有的锁,将线程加入 Condition 队列
- signal():将 Condition 队列的头节点移到 CLH 队列,等待获取锁
【面试官心理】
这道题我能问到 P7 级别,是因为 AQS 是 JUC 的核心框架,理解了 AQS 就理解了 JUC 的半壁江山。能画出 CLH 队列出队/入队流程的候选人说明他真正看过源码。能区分独占模式和共享模式的候选人说明他对 AQS 的设计有系统理解。
1.4 追问升级
追问 1:CLH 队列为什么要从尾部入队、从头部出队?
- 尾部入队:减少与 head 节点的竞争(head 是最接近持有锁的节点,竞争最激烈)
- 头部出队:head 代表已经获取锁的节点,线程被唤醒后直接继承 head 位置(不需要移动节点,只需要替换 head)
这种设计的本质是:CLH 队列中,head 表示当前持有锁的节点(可能已释放但尚未被 GC),tail 表示队列尾部。新节点从 tail 入队,head 的后继节点尝试获取锁。
追问 2:为什么需要 PROPAGATE 状态?
PROPAGATE 是 JDK 9 引入的,用于修复共享模式下的传播问题。
场景:head 释放后唤醒多个节点,但唤醒过程中 head 的状态可能被修改,导致某些节点未收到唤醒。
PROPAGATE 确保唤醒信号能正确传播到所有等待的共享模式节点。
二、ReentrantLock 的 AQS 实现 🟡
2.1 NonfairSync vs FairSync
// 非公平锁的 tryAcquire
protected final boolean tryAcquire(int acquires) {
if (compareAndSetState(0, acquires)) { // 直接 CAS,不检查队列
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return nonfairTryAcquire(acquires);
}
// 公平锁的 tryAcquire
protected final boolean tryAcquire(int acquires) {
if (getState() == 0 && !hasQueuedPredecessors() && // 额外检查:队列中是否有更早的等待者
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
三、生产避坑
3.1 自定义同步器的常见错误
// 错误实现:tryAcquire 中直接修改 state
protected boolean tryAcquire(int arg) {
if (state == 0) {
state = arg; // ❌ 不是原子操作,应该用 CAS
return true;
}
return false;
}
// 正确实现
protected boolean tryAcquire(int arg) {
return compareAndSetState(0, arg);
}
3.2 中断与超时
tryAcquireNanos() 允许限时等待,同时响应中断:
public final boolean tryAcquireNanos(int arg, long nanosTimeout) {
if (Thread.interrupted()) return true; // 先检查中断
return tryAcquire(arg) ||
> doAcquireNanos(arg, nanosTimeout);
}
💡
面试加分点:能说出"JDK 15 引入了 peek() 和 remove() 等方法用于检查 CLH 队列状态,getQueuedLength() 可以获取等待线程数",说明他对 JUC 的演进有跟进。
⚠️
面试陷阱:被问到"AQS 的 state 可以是负数吗",答"不可以"是错的。独占模式下 state 通常 >= 0(重入计数),但共享模式下 tryAcquireShared 可以返回负数表示获取失败,返回正数表示还有剩余资源可继续获取。