学了 ReentrantLock、读写锁、Semaphore、CountDownLatch 之后,可能有过一个疑问:这一堆工具,底层是不是各写各的?不是,它们底下全是同一个东西——AQS(AbstractQueuedSynchronizer,抽象队列同步器)。JUC 里大半的同步工具,都是在这一个框架上搭出来的。这篇把它拆开看:state 是什么、队列怎么排、线程怎么挂起又怎么被唤醒。理解了 AQS,上面那一堆工具就不再是几个孤立的 API,而是同一套机制的不同用法。
上游文章:《ReentrantLock 详解》《读写锁详解》讲用法,这篇讲它们共同的底座。并发全景看《Java 并发编程》。
一、AQS 是什么:一个"抢座位 + 排队"的框架
把 AQS 想象成一个饭馆的座位管理:
- 一个座位标记(state):一个
volatile int,表示资源状态。0 空闲、1 被占;具体什么含义由子类定义——ReentrantLock里是"重入次数",Semaphore里是"剩余许可数",CountDownLatch里是"还差几个"。 - 一个排队队列(CLH 双向队列):抢座失败的线程,排进一条 FIFO 队列,排队期间挂起(park),不占 CPU。
- 一套唤醒规则:座位空出来(state 变了),叫醒队头的下一个线程,让它重新去抢。
AQS 自己不定义"什么算抢到、什么算释放",它只管状态管理 + 排队 + 挂起唤醒这些通用部分;"具体怎么才算抢到锁"由子类实现。这就是类名里"Abstract"的含义——模板方法模式。
二、核心三件套
1. state:一个 volatile int
// AQS 内部的核心字段(简化)
private volatile int state;volatile保证可见性:state 改了,所有线程立刻看到。- 改它用的是 CAS(
Unsafe.compareAndSwapInt):0 → 1只有一个线程能成功,这就是"抢锁"的原子基础。
state 的含义完全由子类定义,这是 AQS 能派生出一堆工具的原因:
| 工具 | state 的含义 |
|---|---|
ReentrantLock | 0 空闲,N 被占用且重入了 N 次 |
Semaphore | 剩余许可数 |
CountDownLatch | 还剩几个计数 |
ReentrantReadWriteLock | 高 16 位读线程数,低 16 位写状态 |
最后一条解释了读写锁怎么用一个 int 管两种锁——把 int 按位拆成两半用。
2. CLH 队列:抢不到的排进双向链表
抢 CAS 失败的线程,被包装成一个 Node,追加到队列尾部:
head → [Node 线程A·等待] ⇄ [Node 线程B·等待] ⇄ [Node 线程C·等待] ← tail
(head 是占位/刚拿到锁的)每个 Node 记着:线程引用、等待状态(waitStatus)、前后指针。排队期间线程被 LockSupport.park() 挂起——操作系统层面不参与调度,零 CPU 消耗,比自旋干等省资源。
3. park / unpark:线程的"停"与"叫醒"
LockSupport.park() 让当前线程挂起,unpark(thread) 叫醒指定线程。比 wait/notify 好用的地方:不需要锁、不需要先 wait 后 notify 的顺序,unpark 可以先发——叫醒信号攒着,线程 park 时直接通过。
三、独占模式:抢锁全流程
以 ReentrantLock 非公平锁为例,线程调 lock() 后发生的事:
// AQS.acquire 的骨架(简化)
public final void acquire(int arg) {
if (!tryAcquire(arg) // 1. 先试着抢(子类实现怎么算抢到)
&& acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 2. 抢不到 → 入队 → 挂起等
selfInterrupt();
}完整走一遍:
- CAS 抢 state:
0 → 1,成功就拿到锁,完事(这就是"非公平"——不用管队列里有没有人在等,先抢一把)。 - 失败 → 入队:包装成 Node 追加到队尾。
- 挂起前再检查一次:看自己的前驱是不是 head——如果队列里自己排第一个,说明轮到自己了,再试一次抢(前驱释放锁的瞬间 state 可能刚空出来)。
- 还不行 →
park()挂起,安静等。 - 持锁线程
unlock()→ state 减到 0 →unpark队列里 head 的下一个节点。 - 被叫醒的线程从 park 处醒来,回到第 3 步再抢。
这张图把全流程画出来:
公平锁和非公平锁差在哪
现在能精确回答这个问题了——差异只在第 1 步:
// 非公平锁的 tryAcquire:上来就 CAS,插队
final boolean tryAcquire(int acquires) {
return (compareAndSetState(0, acquires) && setExclusiveOwnerThread(...)) // 先抢再说
|| nonfairTryAcquire(...);
}
// 公平锁的 tryAcquire:多一个检查——队列里有人排我前面吗?
final boolean tryAcquire(int acquires) {
if (compareAndSetState(0, acquires)) {
// ...
}
// 关键差异:
else if (hasQueuedPredecessors()) // 有人排队 → 我不抢,乖乖去排队
return false;
}非公平锁吞吐量高,就是省掉了"唤醒排队线程"这段开销;代价是刚来的线程可能一直插队,排队的可能饿死。
可重入在哪
也在 tryAcquire 里:CAS 之前先查"state 非零且持锁线程是我自己"→ 是的话 state 直接加 1,不用抢。第一篇实跑验证的 state 累加(0→1→2)就是这条路径。
四、共享模式:state 够减就放行
独占模式是"一个座位",共享模式是"一批座位"——Semaphore、读锁、CountDownLatch.await 都是共享模式:
// AQS.acquireShared 的骨架(简化)
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) // 子类实现:state 够减吗?
doAcquireShared(arg); // 不够 → 入队挂起(和独占类似)
}区别在两点:
- 判断标准:不是
state == 0才成功,而是tryAcquireShared返回"剩余额度"——Semaphore(3)只要 state > 0 就能进,进一个减一个。 - 唤醒会传播:释放时如果资源还有剩余,会连续唤醒队里的一串线程(setHeadAndPropagate),让多个等待者一起进。
实跑验证过:Semaphore(3) 反射读底层 state 初始就是 3,acquire 一次减到 2;4 个线程同时跑,峰值并发恰好 3。
五、AQS 派生了哪些工具
看完机制就明白为什么说"JUC 半壁江山都建在 AQS 上":
| 工具 | 用了 AQS 的什么 |
|---|---|
ReentrantLock | 独占模式,state = 重入次数 |
ReentrantReadWriteLock | 独占 + 共享混合,state 按位拆两半 |
Semaphore | 共享模式,state = 许可数 |
CountDownLatch | 共享模式,state 倒计数 |
ThreadPoolExecutor(Worker) | 独占模式做线程占用标记 |
子类只需要重写几个 tryXxx 方法,排队、挂起、唤醒、并发安全全由 AQS 兜底——这就是模板方法模式的教科书级应用。
小结
- AQS = volatile state(CAS 抢占)+ CLH 双向队列(FIFO 排队)+ park/unpark(挂起唤醒)
- state 含义由子类定义:重入数 / 许可数 / 倒计数 / 读写各半——一个框架派生一个家族
- 独占模式:CAS 抢不到 → 入队 → park → 前驱释放时 unpark 醒来再抢
- 公平 vs 非公平只差一步:非公平上来就 CAS(可插队),公平先查
hasQueuedPredecessors - 可重入 = tryAcquire 里发现持锁线程是自己,state 直接累加
- 共享模式 = state 够减就放行,释放时唤醒会传播
- 子类只写 tryXxx,排队挂起全托管——模板方法模式
到这里,锁系列的主线就通了:用法看《ReentrantLock 详解》和《读写锁详解》,全景地图看《锁的分类与实现原理》,CAS 的底层细节在《Java 并发编程》里也有展开。单机锁管不到多台机器,跨机器的互斥看《Redis 分布式锁详解》。
