AQS · 旁生枝茎
mindmap
root((AQS))
定位
本质
队列同步器
JUC并发基础框架
目标
统一管理同步状态
统一管理等待线程
让上层锁只关注规则
核心思想
volatile state 表示资源
CLH变体双向队列管理阻塞线程
模板方法屏蔽底层细节
核心组成
state
int同步状态
0常表示无锁
compareAndSetState原子修改
同步队列
head哨兵节点
tail尾指针
Node双向链表
失败线程入队等待
Node状态
waitStatus
SIGNAL
CANCELLED
CONDITION
PROPAGATE
阻塞唤醒
LockSupport.park
LockSupport.unpark
获取与释放
独占模式
acquire
tryAcquire
失败则入队
acquireQueued自旋加阻塞
release
tryRelease
成功后唤醒后继
共享模式
acquireShared
tryAcquireShared
返回值决定是否传播
releaseShared
tryReleaseShared
doReleaseShared传播唤醒
模板方法
子类只实现tryXxx
流程控制由AQS负责
队列运行机制
入队
CAS尾插
enq自旋直到成功
抢锁
只让前驱是head的节点再尝试
失败则park
唤醒
当前节点释放后唤醒后继
跳过取消节点
公平与非公平
非公平
先CAS抢一次
抢不到再排队
公平
先看队列前面是否有人
有人则老实排队
Condition条件队列
作用
将等待条件的线程暂存到另一条队列
实现await与signal
await流程
释放当前持有锁
进入Condition队列
被signal后转回同步队列
重新竞争锁
signal流程
从Condition队列转移节点
放入同步队列等待抢锁
前提
必须配合独占锁
典型是ReentrantLock
常见子类
ReentrantLock
独占
可重入
公平非公平
Semaphore
共享
state表示许可数
CountDownLatch
共享
state表示剩余计数
ReentrantReadWriteLock
读共享写独占
一个state拆高低位
FutureTask
用到AQS思想管理完成与等待
高频面试点
AQS是什么
state加队列加CAS加park/unpark
为什么用CLH变体队列
降低竞争
有序管理等待线程
为什么前驱是head才能抢锁
减少无效竞争
保证队列语义
公平锁一定公平吗
只是更公平
调度层面不绝对
Condition和Object.wait区别
一个配合Lock
一个配合synchronized
Condition可多个等待队列
可重入怎么实现
同线程重复获取时state累加
共享模式核心区别
获取成功后可能继续传播唤醒
易错点
AQS不是锁
它是锁和同步器的底座
state不一定表示1把锁
也可能表示重入次数或许可证数量
不是纯自旋
失败后会阻塞挂起
Condition不是直接唤醒就运行
只是转移回同步队列
还要重新抢锁
取消节点不会立即消失
会在后续流程中被跳过清理
1. AQS 到底是什么
AQS 全称是 AbstractQueuedSynchronizer。
它是一个 同步器框架,用来帮助开发者快速实现各种锁和同步组件。
你可以把它理解成:
AQS 提供了一套通用的“排队 + 竞争状态 + 阻塞唤醒”机制,具体这个同步器是锁、信号量、倒计时器,交给子类自己定义。
所以 AQS 干的事情主要有三件:
-
用一个 state 表示同步状态
-
用一个双向队列管理抢不到锁的线程
-
提供 acquire / release 这类模板方法,子类只要实现 tryXxx 即可
2. 为什么 AQS 这么重要
因为 Java 里很多并发工具底层都靠它:
-
ReentrantLock -
Semaphore -
CountDownLatch -
ReentrantReadWriteLock -
ConditionObject
也就是说,AQS 有点像一个 并发世界的脚手架。
你不需要每次都手搓:
-
谁抢锁成功
-
谁入队等待
-
谁该阻塞
-
谁该被唤醒
-
队列怎么维护
这些脏活累活 AQS 给你包了。
3. AQS 的设计思想
AQS 的思想很优雅,核心是 模板方法模式。
它把同步器拆成两层:
第一层:通用框架层
AQS 自己负责:
-
state状态管理 -
等待队列管理
-
线程入队
-
线程挂起
park -
线程唤醒
unpark -
独占 / 共享模式的流程控制
第二层:子类自定义规则
子类只需要告诉 AQS:
-
当前这个同步器,怎么判断能不能拿到资源
-
当前这个同步器,怎么释放资源
比如这些方法:
代码块收起展开
protected boolean tryAcquire(int arg)
protected boolean tryRelease(int arg)
protected int tryAcquireShared(int arg)
protected boolean tryReleaseShared(int arg)
protected boolean isHeldExclusively()也就是说:
AQS 说,排队和调度我来做,资源能不能拿你来说。
这个分工特别妙。
4. AQS 最核心的两个东西
4.1 state,同步状态
AQS 里有一个非常关键的变量:
private volatile int state;
它表示同步状态。
这个状态的含义由子类决定。
不同组件里 state 的含义不同
ReentrantLock
-
0表示没锁 -
1表示已被某线程持有 -
大于
1表示重入次数
Semaphore
state表示剩余许可证数量
CountDownLatch
state表示剩余倒计时次数
ReentrantReadWriteLock
state高低位拆分,分别表示读锁和写锁状态
所以你要记住一句:
AQS 只提供 state 这个容器,具体语义由子类赋予。
4.2 CLH 变体双向等待队列
AQS 内部维护了一个 FIFO 双向链表队列,一般说它是基于 CLH queue 思想实现的变体。
它的作用是:
-
抢资源失败的线程进队
-
按顺序排队等待
-
前驱节点释放后,唤醒后继节点
队列节点大概长这样:
代码块收起展开
static final class Node {
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
}几个关键字段:
-
prev:前驱节点 -
next:后继节点 -
thread:当前节点绑定的线程 -
waitStatus:当前节点等待状态 -
nextWaiter:用于区分独占/共享模式,或者条件队列
5. AQS 队列里的 Node 是什么
每个等待线程会被包装成一个 Node 节点。
你可以理解成:
-
线程抢锁失败
-
AQS 给它发一个号码牌
Node -
让它去队尾排队
-
等前面的人处理完,再轮到它
6. waitStatus 是什么
这个字段面试里也挺常见。
常见值有这些:
-
0:默认状态 -
SIGNAL = -1:当前节点的后继需要被唤醒 -
CANCELLED = 1:线程取消等待 -
CONDITION = -2:节点在条件队列中 -
PROPAGATE = -3:共享模式下传播唤醒
你不用死记所有值,先抓主线:
最重要的两个
-
SIGNAL:告诉前驱,释放时记得唤醒我后面的线程 -
CANCELLED:这个节点废了,别管它了
7. AQS 的两种模式
AQS 支持两种资源获取模式:
7.1 独占模式 Exclusive
同一时刻只能一个线程拿到资源。
比如:
ReentrantLock
7.2 共享模式 Shared
同一时刻可以多个线程一起拿资源。
比如:
-
Semaphore -
CountDownLatch的 await 唤醒传播 -
ReentrantReadWriteLock的读锁
这个分类非常关键,因为 AQS 的很多方法都分两套:
-
acquire / release对应独占 -
acquireShared / releaseShared对应共享
8. AQS 获取锁的整体流程
先讲最经典的 独占获取流程。
以 acquire(int arg) 为例。
8.1 第一阶段,先直接抢
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
代码块收起展开
selfInterrupt();
}看这段代码,AQS 的思路非常清晰:
第一步
先调用子类实现的 tryAcquire(arg)
-
成功,直接拿到资源,结束
-
失败,说明有竞争,进入队列流程
这就是典型的 先抢一次,失败再排队。
8.2 第二阶段,失败就入队
addWaiter(Node.EXCLUSIVE)
这一步会把当前线程封装成一个独占节点,插入等待队列尾部。
入队底层用的是 CAS 保证线程安全。
队列一般会有一个哨兵头节点 head,真正等待的线程从 head.next 开始。
8.3 第三阶段,入队后自旋等待
acquireQueued(node, arg)
这个方法很重要,它会让线程在队列里循环判断:
逻辑大致是
-
看看自己前驱是不是头节点
-
如果前驱是头节点,说明自己有资格再试一次抢锁
-
调用
tryAcquire -
成功则自己变成新 head
-
失败则决定是否阻塞
-
阻塞后等前驱释放时唤醒自己
这里的思想很妙:
只有排在队头的线程,才有资格再次抢资源。
这样可以减少无意义竞争。
9. 为什么要阻塞,不一直自旋吗
你知道吗,这就是 AQS 很聪明的地方 ❤️
CAS 那类乐观锁喜欢一直自旋。
AQS 的场景更复杂,如果竞争持续很久,一直空转会把 CPU 烧得很难看。
所以 AQS 采用的是:
-
短暂自旋检查
-
条件合适就 park 挂起
-
前驱释放时再 unpark 唤醒
这属于 自旋 + 阻塞 的结合体。
10. park / unpark 是什么
AQS 挂起线程用的是 LockSupport:
-
LockSupport.park()挂起当前线程 -
LockSupport.unpark(thread)唤醒指定线程
你可以把它理解成并发包里的“精准停车和叫醒”。
比 wait/notify 更底层,也更灵活。
11. 独占模式完整流程图
你脑子里可以这样想:
线程进来
↓
tryAcquire 尝试抢资源
↓
成功 -> 直接返回
↓
失败
↓
封装成 Node 加入队尾
↓
循环判断:
前驱是不是 head?
是 -> 再次 tryAcquire
成功 -> 自己设为 head,出队
失败 -> park 挂起
否 -> park 挂起
↓
前驱释放资源时 unpark 后继
↓
被唤醒后继续循环尝试
12. AQS 释放锁的流程
独占释放是 release(int arg):
代码块收起展开
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;代码块收起展开
if (h != null && h.waitStatus != 0) 代码块收起展开
unparkSuccessor(h);
return true;
}
return false;
}流程是:
第一步
调用子类实现的 tryRelease(arg)
子类决定:
-
资源是否真的完全释放了
-
如果是可重入锁,是减计数还是彻底释放
第二步
如果确实释放成功,就唤醒 head 的后继节点
也就是:
-
当前持锁线程放手
-
叫醒排在最前面的等待线程来继续竞争
13. 为什么释放时唤醒的是后继节点
因为等待队列本质上是 FIFO。
AQS 想尽量保证:
- 排前面的先获得再次尝试的资格
注意是 先唤醒先尝试,并不代表 100% 绝对公平,除非具体实现就是公平锁。
14. 公平锁和非公平锁在 AQS 里怎么体现
这个很经典。
非公平锁
线程一上来就先直接抢,不太管队列里有没有人。
比如 ReentrantLock 默认就是非公平锁。
好处:
-
吞吐量高
-
少一次排队判断
坏处:
- 可能插队
公平锁
线程来时先看队列前面有没有人在等。
有的话,自己老实排队。
AQS 里通常通过 hasQueuedPredecessors() 来判断前面是否已有排队线程。
15. ReentrantLock 是怎么基于 AQS 实现的
这是理解 AQS 最好的例子。
ReentrantLock 内部有一个 Sync,它继承 AQS。
非公平锁 tryAcquire 逻辑核心思路
情况 1:state == 0
说明锁空闲,尝试 CAS 抢占:
-
CAS 成功,设置 owner 为当前线程
-
获取成功
情况 2:锁已被当前线程持有
说明发生重入:
-
state++
-
成功
情况 3:锁被别人持有
-
失败
-
进入 AQS 队列等待
所以 ReentrantLock 的重入本质就是:
同一线程再次获取锁时,发现 owner 还是自己,于是只增加 state 计数。
释放时则反过来:
-
state—
-
直到减到 0,才真正释放锁并唤醒后继
16. CountDownLatch 为什么也能用 AQS
这个点很多人第一次看会觉得神奇。
CountDownLatch 用的是 共享模式。
state 的含义
- 表示剩余计数
await()
调用 acquireSharedInterruptibly(1)
如果 state != 0,当前线程进队等待。
countDown()
调用 releaseShared(1)
把 state 减 1。
当减到 0 时,说明条件满足,唤醒所有等待线程。
所以它不是“抢锁”,它是在复用 AQS 的:
-
state 管理
-
共享队列唤醒机制
17. Semaphore 为什么能用 AQS
Semaphore 也走共享模式。
state 的含义
- 当前可用许可证数量
acquire()
尝试把 state 减掉一个许可数
-
成功,拿到许可证
-
失败,说明没资源,入队等待
release()
把 state 加回去,然后唤醒后继节点继续竞争
所以共享模式下,大家竞争的不是“唯一锁”,而是“若干份资源”。
18. 共享模式和独占模式的区别到底在哪
独占模式
-
同一时刻一个线程成功
-
成功后其他线程继续等
-
比如互斥锁
共享模式
-
一次可能多个线程都成功
-
一个线程成功后,可能还要继续传播唤醒后继
-
比如信号量、读锁
所以共享模式里有个很关键的概念:
传播 propagation
如果资源还有剩余,就可以继续唤醒后面节点。
这就是 PROPAGATE 状态出现的背景。
19. Condition 是什么,和 AQS 什么关系
这个点超重要。
Condition 是用来做 条件等待队列 的。
通常和 Lock 配合使用,类似 Object.wait/notify 的升级版。
比如:
代码块收起展开
lock.lock();
try {
while (!conditionOK) {
condition.await();
}代码块收起展开
// do something 代码块收起展开
} finally {
lock.unlock();
}AQS 里的 ConditionObject 是其内部类。
20. Condition 的本质
AQS 其实维护了两类队列:
1. 同步队列
抢锁失败的线程排这里
2. 条件队列
调用 await() 后,线程先进条件队列
条件队列里的线程特点
-
它们在等某个条件成立
-
暂时不参与抢锁
当别的线程调用 signal() 时:
-
条件队列头节点被转移到同步队列
-
然后去重新参与锁竞争
这个转移过程是 Condition 的精髓。
21. await 和 signal 流程
await()
线程当前必须先持有锁,然后:
-
把当前线程包装成条件节点,加入条件队列
-
释放当前持有的锁
-
挂起自己
-
等待被 signal
-
被转移到同步队列
-
重新竞争锁
-
拿到锁后从 await 返回
signal()
当前线程必须持有锁,然后:
-
从条件队列取出一个节点
-
转移到同步队列
-
后续让它参与正常的抢锁流程
所以:
await 不是直接唤醒后立刻执行,而是先回到同步队列重新抢锁。
这个非常容易考。
22. AQS 为什么要用双向链表
因为它需要高效处理这些操作:
-
从尾部入队
-
找前驱节点
-
取消节点时跳过失效节点
-
唤醒后继节点
单链表做这些会麻烦很多,尤其取消和回溯处理。
23. AQS 为什么 head 节点通常不存真实线程
因为 head 更多像一个 哨兵节点。
当一个线程成功获取资源后,它会把自己所在节点设为新的 head。
原来的头节点会被丢弃。
head 的主要作用是:
-
标识队列起点
-
简化队列逻辑
-
让
head.next成为实际等待的第一个线程
24. AQS 和 synchronized 的区别
这个面试也常问。
synchronized
-
JVM 层面的关键字
-
底层依赖对象监视器
monitor -
使用更简单
-
自动释放锁
-
支持可重入
-
条件等待靠
wait/notify
AQS
-
JDK 层面的同步器框架
-
基于 CAS + volatile + CLH 队列 + park/unpark
-
扩展性更强
-
可以实现公平锁、非公平锁、共享锁、条件队列等复杂语义
你可以这么理解:
synchronized更像现成家具,AQS 更像高质量工具箱。
25. AQS 底层靠哪些技术拼起来
这个可以当总结图记住:
volatile
保证 state、head、tail 等变量的可见性
CAS
保证入队、出队、修改 state 时的原子性
CLH 变体队列
管理等待线程顺序
LockSupport
实现线程阻塞和唤醒
这四个点一起组成了 AQS 的骨架。
26. AQS 常见源码级核心方法
你不用全背,先认脸熟。
独占相关
-
acquire(int arg) -
release(int arg) -
tryAcquire(int arg) -
tryRelease(int arg)
共享相关
-
acquireShared(int arg) -
releaseShared(int arg) -
tryAcquireShared(int arg) -
tryReleaseShared(int arg)
队列相关
-
addWaiter(Node mode) -
enq(Node node) -
acquireQueued(Node node, int arg) -
unparkSuccessor(Node node)
条件队列相关
-
await() -
signal() -
signalAll()
27. 为什么说 AQS 是模板方法模式
因为它把主流程写死了:
-
获取失败怎么办
-
入队怎么办
-
挂起怎么办
-
释放后唤醒谁
这些都由父类 AQS 控制。
子类只负责覆盖几个钩子方法:
-
tryAcquire -
tryRelease -
tryAcquireShared -
tryReleaseShared
这就是非常典型的模板方法。
28. AQS 的一个极简伪代码理解版
获取独占锁
代码块收起展开
public final void acquire(int arg) {
if (!tryAcquire(arg)) {
Node node = addWaiter(EXCLUSIVE);
boolean interrupted = false;
for (;;) {
Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
break;
}
if (shouldParkAfterFailedAcquire(p, node)) {
park();
}
if (Thread.interrupted()) {
interrupted = true;
}
}
if (interrupted) selfInterrupt();
}
}释放独占锁
代码块收起展开
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null) {
unparkSuccessor(h);
}
return true;
}
return false;
}你看到这里应该有感觉了:
AQS 的世界,其实一直在反复做这件事:
抢资源,失败入队,挂起等待,被唤醒后再抢。
29. 面试最容易问的高频点
AQS 是什么
AQS 是一个同步器框架,通过 state 和 FIFO 等待队列来实现线程同步,很多锁和同步组件都基于它实现。
AQS 的核心组成
-
volatile state
-
CLH 变体双向队列
-
CAS
-
LockSupport
AQS 支持哪两种模式
-
独占模式
-
共享模式
AQS 和 ReentrantLock 的关系
ReentrantLock 内部的 Sync 继承 AQS,通过重写 tryAcquire 和 tryRelease 实现可重入互斥锁。
Condition 和 AQS 的关系
ConditionObject 是 AQS 的内部条件队列实现,await 会进入条件队列,signal 会把节点转移到同步队列重新竞争锁。
公平锁和非公平锁区别
公平锁会先检查队列中是否已有前驱节点,非公平锁会直接尝试 CAS 抢占。
30. 你可以直接背的面试版回答
版本一,正常详细版
AQS 全称 AbstractQueuedSynchronizer,是 JUC 中用来构建锁和同步器的基础框架。它的核心思想是用一个 volatile 修饰的 state 表示同步状态,用一个 FIFO 双向等待队列管理获取同步状态失败的线程,并结合 CAS 和 LockSupport 实现原子更新、线程阻塞与唤醒。AQS 支持独占和共享两种模式。像 ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 都是基于 AQS 实现的。AQS 本身定义了 acquire、release 等模板方法,子类只需要重写 tryAcquire、tryRelease 等方法来自定义资源获取和释放规则。
版本二,简洁版
AQS 就是一个同步器脚手架。它负责排队、阻塞、唤醒和状态管理,子类只负责定义怎样算获取成功、怎样算释放成功。底层核心是 state、CLH 队列、CAS 和 park/unpark。
31. 学 AQS 时最容易晕的几个点
点 1:AQS 不是锁
AQS 是“造锁框架”,锁是它的子类实现。
点 2:state 不是固定表示锁数量
state 是通用同步状态,含义取决于子类。
点 3:入队后不会一直抢
只有接近队头的线程才有资格重点尝试,其他线程会 park。
点 4:signal 不是立刻执行
signal 只是把条件队列的节点转移回同步队列,后面还要重新抢锁。
点 5:共享模式会传播
一个线程成功后,可能继续唤醒后继,让多个线程一起通过。
32. 一张脑内总图
你可以把 AQS 想成这个模型:
├─ state:资源状态
├─ 同步队列:抢资源失败的线程排队
├─ 条件队列:await 的线程暂存
├─ acquire/release:主流程模板
├─ tryAcquire/tryRelease:子类自定义规则
├─ CAS:改状态、入队时保证原子性
└─ park/unpark:线程挂起与唤醒
33. 你当前阶段最该掌握的主线
Gilbert,现阶段你先把这 6 句拿稳,就已经很能打了:
-
AQS 是同步器框架,不是具体锁
-
核心变量是 volatile state
-
抢不到资源的线程会进入 FIFO 双向队列
-
独占模式常见于 ReentrantLock,共享模式常见于 Semaphore、CountDownLatch
-
底层依赖 CAS + CLH 队列 + LockSupport
-
Condition 有独立条件队列,被 signal 后会回到同步队列重新抢锁
回到主线
速通
AQS(AbstractQueuedSynchronizer)
一、AQS是什么
JUC中绝大多数同步工具的底层骨架。它提供了一套通用的框架:一个状态变量 + 一个等待队列 + 一组模板方法。子类只需要定义”获取和释放的语义”,排队、阻塞、 唤醒这些脏活AQS全包了。
基于AQS的实现:
- ReentrantLock → state表示重入次数
- CountDownLatch → state表示剩余计数
- Semaphore → state表示剩余许可
- ReentrantReadWriteLock → state高16位读锁计数,低16位写锁计数
二、两个核心组件
- state(int类型)
- 用volatile修饰,保证可见性
- 通过CAS修改,保证原子性
- 语义由子类定义,AQS本身不关心state代表什么
- 提供三个方法操作:getState()、setState()、compareAndSetState()
- CLH等待队列(变体)
- 一个双向链表,每个节点是一个Node,代表一个等待中的线程
- Node的关键字段:
- thread:等待的线程引用
- prev:前驱节点
- next:后继节点
- waitStatus:节点的等待状态
- 0:初始状态
- SIGNAL(-1):表示”我释放后需要唤醒后面的节点”
- CANCELLED(1):线程取消了,跳过这个节点
- CONDITION(-2):在Condition队列中等待
- PROPAGATE(-3):共享模式下,释放需要向后传播
- 有一个虚拟的head节点(哨兵),不代表任何线程
- tail指向队尾
head(哨兵) ⇄ Node(线程A) ⇄ Node(线程B) ⇄ Node(线程C) = tail
三、独占模式完整流程(以ReentrantLock为例)
获取锁:acquire(1)
代码块收起展开
acquire(int arg) {
if (!tryAcquire(arg) && // 第一步:尝试获取
acquireQueued( // 第三步:在队列里自旋+阻塞
addWaiter(Node.EXCLUSIVE) // 第二步:入队
, arg))
selfInterrupt();
}第一步:tryAcquire()——子类实现,尝试一次获取
以非公平锁为例:
- 读state,如果state == 0,说明没人占,CAS把state改成1
- CAS成功 → 设置exclusiveOwnerThread为当前线程,返回true,拿到了
- 如果state != 0,检查exclusiveOwnerThread是不是自己 → 是自己就state+1(重入),返回true
- 都不是 → 返回false,获取失败
第二步:addWaiter()——tryAcquire失败,把自己包装成Node加入队尾
- 创建一个Node,模式为EXCLUSIVE,thread为当前线程
- CAS把这个Node设为新的tail,和前驱节点建立双向链接
- 如果CAS失败(别的线程也在入队),进入enq()用自旋+CAS保证一定入队成功
- 如果队列还没初始化(head和tail都是null),先创建一个哨兵节点作为head
第三步:acquireQueued()——入队后,自旋尝试+阻塞等待
代码块收起展开
for (;;) { // 死循环
Node p = node.predecessor(); // 拿前驱
if (p == head && tryAcquire(arg)) {代码块收起展开
// 我的前驱是head(我排在第一个),再尝试一次获取
// 成功了!把自己设为新的head,清理旧head代码块收起展开
setHead(node);
p.next = null; // help GC
return;
}代码块收起展开
// 获取失败,判断是否应该阻塞
if (shouldParkAfterFailedAcquire(p, node) && //把前驱的waitStatus设为SIGNAL parkAndCheckInterrupt()) // LockSupport.park() 阻塞自己 interrupted = true; // 被唤醒后,回到循环顶部,再试一次tryAcquire }
关键细节:
- 不是入队就立刻阻塞,而是先看自己是不是排第一个,如果是就再试一次tryAcquire
- shouldParkAfterFailedAcquire():把前驱节点的waitStatus设为SIGNAL(-1),意 思是”前驱释放时记得叫醒我”,然后返回true表示可以安心park了
- park之后线程就挂起了,CPU不消耗,直到被unpark唤醒
- 唤醒后不是直接拿到锁,而是回到for循环再次tryAcquire竞争
释放锁:release(1)
代码块收起展开
release(int arg) {
if (tryRelease(arg)) { // state-1,如果state归零了返回true
Node h = head;代码块收起展开
if (h != null && h.waitStatus != 0)代码块收起展开
unparkSuccessor(h); // 唤醒head后面的第一个有效节点
return true;
}
return false;
}- tryRelease():state减1。如果减完后state == 0,说明锁完全释放(重入全部退出),清除exclusiveOwnerThread,返回true。如果s tate还大于0(还有重入没退),返回false,不唤醒任何人
- unparkSuccessor():从head的next开始找,找到第一个waitStatus不是CANCELLED 的节点,调LockSupport.unpark()唤醒它的线程
- 那个线程从park处醒来,回到acquireQueued的for循环,tryAcquire竞争锁
四、公平与非公平
唯一的区别在tryAcquire()里:
- 非公平: 线程来了直接CAS抢state,不管队列里有没有人排着
- 公平: 线程来了先调hasQueuedPredecessors()看队列里有没有人排在自己前面,有 人就不抢,乖乖入队排队
非公平性能更好,因为减少了线程切换的开销——新来的线程可能直接拿到锁,不用入队 park再unpark。但可能导致队列里的线程长期拿不到锁(饥饿)。
五、共享模式(CountDownLatch、Semaphore用的)
和独占模式的区别:
- 独占模式:一个线程获取成功后,其他线程必须等
- 共享模式:多个线程可以同时获取成功
获取:acquireShared()
- 调tryAcquireShared() → 返回值 ≥ 0表示成功,< 0表示失败
- 失败就入队阻塞,和独占模式类似
- 关键区别:一个线程被唤醒拿到资源后,如果还有剩余资源,会继续唤醒后面的节 点(传播唤醒),而不是像独占模式那样只唤醒一个
释放:releaseShared() 4. 调tryReleaseShared() 修改state 5. 唤醒等待队列中的节点 6. CountDownLatch中:state减到0,唤醒所有等待的线程 7. Semaphore中:state加1,唤醒一个等待的线程
六、Condition队列
ReentrantLock的newCondition()返回的就是AQS内部的ConditionObject,提供类似Obj ect.wait()/notify()的能力:
- 每个Condition维护一个独立的单向等待队列,和AQS的CLH队列是两个队列
- condition.await():释放锁,把当前线程从CLH队列移到Condition队列,park阻塞
- condition.signal():把Condition队列的头节点移回CLH队列尾部,等待重新竞争锁
- CyclicBarrier就是用ReentrantLock + Condition实现的:所有线程await到Conditi on队列,最后一个到的线程signalAll全部移回CLH队列
CLH等待队列: head ⇄ NodeA ⇄ NodeB Condition队列1: NodeC → NodeD Condition队列2: NodeE
signal(): Condition队列 → CLH队列 await(): CLH队列 → Condition队列
这就是AQS的全貌。state + CLH队列是骨架,tryAcquire/tryRelease是子类填的肉, 独占/共享是两种模式,Condition是附加能力。