本文目录导读:

- 目录导读
- AQS是什么?为什么它是JUC的基石
- 核心原理:state变量 + CLH变体队列的协同机制
- 源码级拆解:acquire与release的完整流程
- 案例实战:基于AQS手写一个不可重入锁
- 常见问题问答(FAQ)
- AQS设计哲学与性能边界
目录导读
- AQS是什么?为什么它是JUC的基石
- 核心原理:state变量 + CLH变体队列的协同机制
- 源码级拆解:acquire与release的完整流程
- 案例实战:基于AQS手写一个不可重入锁
- 常见问题问答(FAQ)
- AQS设计哲学与性能边界
AQS是什么?为什么它是JUC的基石
AQS(AbstractQueuedSynchronizer)是Java并发包(java.util.concurrent)中所有锁和同步器的基础抽象类,ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock等核心工具,内部均继承或组合了AQS,它的本质是一个提供“状态位+等待队列”的同步框架,让开发者只需重写几个钩子方法,就能实现自定义同步器。
核心三要素:
volatile int state:同步状态,例如锁的持有次数、信号量剩余许可数。CLH变体双向队列:存储竞争失败的线程节点(Node),通过自旋+park/unpark管理阻塞与唤醒。- 模板方法模式:
acquire()、release()为模板,tryAcquire()、tryRelease()为钩子,子类只需定义“如何改变状态”即可。
核心原理:state变量 + CLH变体队列的协同机制
CLH锁在原始设计中是单向链表,AQS将其改造为双向链表 + 尾插法,并增加状态标记(CANCELLED=1、SIGNAL=-1、CONDITION=-2、PROPAGATE=-3),关键改进点:
- 避免惊群:每个节点只唤醒其后继节点,而非广播唤醒所有线程。
- 支持中断与超时:通过节点状态和LockSupport.parkNanos实现。
- 公平/非公平策略:非公平锁在
tryAcquire中直接CAS抢state,不公平但吞吐量高;公平锁则检查队列中是否有前驱节点。
入队与唤醒的微观过程:
- 线程A持有锁,state=1。
- 线程B尝试获取失败,封装为Node,CAS尾插队列,并设置前驱节点为SIGNAL。
- 线程B调用
LockSupport.park()挂起。 - 线程A释放锁(state=0),唤醒队列头部后继节点(线程B)。
- 线程B醒来后,再次CAS尝试获取锁,并移除自己的节点。
源码级拆解:acquire与release的完整流程
// AQS.acquire() 模板
public final void acquire(int arg) {
if (!tryAcquire(arg)) { // 钩子:子类实现
Node node = addWaiter(Node.EXCLUSIVE); // 入队
acquireQueued(node, arg); // 循环自旋/park
}
}
关键点分析:
addWaiter:快速CAS尾插,失败则用enq()自旋保证入队成功。acquireQueued:循环内先检查前驱是否为head,若是则再次尝试tryAcquire;否则应设置前驱SIGNAL后park。- 中断处理:若线程被中断,仅在退出时补偿
selfInterrupt(),保证不丢失中断状态。
释放流程:
public final boolean release(int arg) {
if (tryRelease(arg)) { // 钩子:将state减为0
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒后继
return true;
}
return false;
}
案例实战:基于AQS手写一个不可重入锁
public class Mutex implements Lock {
private final Sync sync = new Sync();
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
@Override public void lock() { sync.acquire(1); }
@Override public void unlock() { sync.release(1); }
// 其他接口方法略...
}
测试验证:启动10个线程,每个线程对共享变量做1000次累加,使用上述锁后,最终结果必为10000,证明互斥性正确。
常见问题问答(FAQ)
Q1:AQS为什么用双向链表?单向不行吗?
A:双向链表便于高效取消节点(将前驱后继直接连接)和扫描队列(如处理超时或中断),单向链表这些操作需O(n)复杂度。
Q2:非公平锁性能一定优于公平锁吗?
A:不一定,非公平锁减少线程唤醒切换,吞吐量更高;但在高竞争下可能造成饥饿,公平锁则牺牲部分性能换取公平性,实际选择取决于业务场景(如交易系统需公平,秒杀系统可非公平)。
Q3:state变量为什么必须volatile?
A:因为多线程CAS操作依赖volatile保证可见性,否则一个线程修改后,其他线程可能看不到最新状态,导致锁失效。
Q4:CLH队列中的Park和Unpark开销大吗?
A:与自旋锁相比,park/unpark会涉及内核态切换,性能开销较大,但能避免CPU空转,JDK后续加入了AbstractQueuedLongSynchronizer及轻量级锁优化,但AQS仍是核心框架。
AQS设计哲学与性能边界
AQS的精华在于将“阻塞-唤醒”与“状态管理”解耦,通过模板模式定制化扩展,它实现了三个关键平衡:
- 空间换时间:队列节点缓存线程引用,减少线程重建。
- 公平与吞吐的权衡:通过钩子让子类自行决策。
- 中断/超时机制的透明集成:统一在acquireQueued中处理。
性能边界:AQS在低竞争(1-4线程)下表现优秀(CAS几乎成功);高竞争下由于线程频繁阻塞唤醒,性能下降明显,此时可考虑LongAdder或Phaser等更细粒度工具。
理解AQS不仅是应对面试的“八股文”,更是进阶高并发编程的分水岭——读透这份源码,你会对Java并发模型的底层心跳有更加深刻的感知。