一、AQS 是什么?
一句话定义 :
AQS 是 Java 并发包的"扫地僧",默默支撑了 ReentrantLock
、Semaphore
、CountDownLatch
等一众同步器的底层实现。
核心思想:
- 用 state 表示资源 :比如锁的持有次数(
ReentrantLock
)或剩余许可证数量(Semaphore
)。 - 用队列管理线程:抢不到资源的线程排队等待,避免无脑自旋浪费 CPU(类似奶茶店叫号系统)。
经典比喻:
- state:奶茶店剩余的"椰果奶茶"数量。
- CLH 队列:排队等奶茶的小哥哥小姐姐们(线程)。
- CAS 操作:店员(CPU)用"无接触扫码枪"快速处理订单(线程竞争)。
二、AQS 核心数据结构:状态 + 队列
1. state(资源状态)
- volatile int:保证多线程可见性。
- 具体含义由子类定义 :
ReentrantLock
:state=0 表示未加锁,>0 表示锁被重入的次数。Semaphore
:state 表示剩余许可证数量。CountDownLatch
:state 表示倒计时的初始值。
2. CLH 队列(线程排队系统)
- 双向链表:每个节点(Node)封装一个等待线程。
- 设计目标 :
- 公平性:先来后到(公平模式下)。
- 高效唤醒:快速找到下一个可执行的线程。
- 节点状态(waitStatus) :
CANCELLED(1)
:舔狗放弃等待(比如线程超时或中断)。SIGNAL(-1)
:当前节点释放资源后需要唤醒下一个节点。CONDITION(-2)
:节点在条件队列中等待(如Condition.await()
)。
三、AQS 工作流程:以 ReentrantLock 为例
1. 加锁(acquire)
java
// ReentrantLock.NonfairSync 的 lock() 方法(非公平模式)
final void lock() {
if (compareAndSetState(0, 1)) // 直接插队尝试抢锁
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 进入AQS排队逻辑
}
// AQS 的 acquire() 方法
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 子类实现抢锁逻辑
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
步骤拆解:
- tryAcquire():子类自定义抢锁逻辑(比如非公平锁直接插队)。
- addWaiter():将线程包装为 Node,加入队列尾部(CAS 保证线程安全)。
- acquireQueued():在队列中自旋或阻塞,等待被唤醒。
2. 解锁(release)
java
// ReentrantLock 的 unlock() 方法
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;
}
关键操作:
- unparkSuccessor():找到队列中第一个未取消的节点,唤醒其关联的线程。
四、源码级细节:舔狗线程的"自旋与阻塞"
1. acquireQueued() 的"舔狗循环"
java
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 如果前驱是头节点,尝试抢锁(体现公平性)
if (p == head && tryAcquire(arg)) {
setHead(node); // 抢到锁后,自己变成头节点
p.next = null;
failed = false;
return interrupted;
}
// 是否需要阻塞?
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
核心逻辑:
- 自旋检查:前驱节点是否是头节点?能否抢到锁?
- 阻塞 :通过
LockSupport.park()
让线程休眠,避免 CPU 空转。
2. shouldParkAfterFailedAcquire():舔狗的自我修养
java
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) // 前驱节点会通知我
return true;
if (ws > 0) { // 前驱节点已取消,跳过它
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 设置前驱节点状态为 SIGNAL(让它记得唤醒我)
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
本质:确保自己能被正确唤醒,同时清理队列中已取消的节点。
五、AQS 设计哲学总结
1. 模板方法模式
- 父类搭骨架:AQS 实现了排队、阻塞、唤醒的通用逻辑。
- 子类填血肉 :子类只需实现
tryAcquire
、tryRelease
等钩子方法。
2. 兼顾公平与效率
- 非公平模式:允许插队,减少线程切换开销。
- 公平模式:严格排队,避免线程饿死。
3. 用 CAS 替代锁
- 无锁化设计:通过 CAS 修改 state 和队列指针,减少锁竞争。
六、高频面试题
1. 为什么 AQS 用双向链表?
- 答:方便快速删除已取消的节点(如超时或中断时)。单向链表只能从头遍历,双向链表支持逆向操作。
2. AQS 的共享模式与独占模式有什么区别?
- 独占模式 :资源只能被一个线程持有(如
ReentrantLock
)。 - 共享模式 :资源可被多个线程共享(如
Semaphore
),唤醒时会传播信号(如doReleaseShared()
)。
3. AQS 如何实现可重入锁?
- 答 :通过
state
记录重入次数,每次重入 state+1,释放时 state-1,直到 state=0 才完全释放锁。
七、总结:AQS 是并发领域的"乐高积木"
- 高扩展性:基于 AQS 可轻松实现各种同步工具。
- 高性能:通过 CAS + CLH 队列减少锁竞争。
- 高智商陷阱:用错了容易导致死锁或性能问题。
最后忠告:
"若你理解了 AQS,Java 并发就掌握了一半;
若你精通了 AQS,面试官会颤抖着给你发 offer!"