AQS:Java 并发界的 “包工头”,管着线程排队干活的狠角色
如果你写 Java 并发代码时,用过ReentrantLock锁门、CountDownLatch倒计时,那你早就在 “偷偷” 用 AQS 了 —— 这货是 Java 并发包(JUC)的 “幕后老板”,ReentrantLock这些工具都是它手下的 “小组长”,干活全靠 AQS 定的规矩。
但 AQS 这名字太劝退了:AbstractQueuedSynchronizer(抽象队列同步器),光念完都得喘口气。今天咱们不用术语堆料,就把 AQS 当成 “工地包工头”,聊聊它是怎么管着一群 “线程工人” 抢资源、干活、排队的。
一、先搞懂:AQS 到底是干啥的?
你可以把 AQS 理解为 “并发界的万能模板”—— 它自己不干活,但给所有 “锁” 和 “同步工具” 画好了干活的框架:
- 比如你要做个 “锁”(像ReentrantLock),AQS 说:“别瞎琢磨了,我这儿有‘资源状态表’和‘排队队伍’,你只要告诉我‘怎么抢资源’‘怎么还资源’就行。”
- 比如你要做个 “倒计时器”(像CountDownLatch),AQS 说:“同样的配方,状态表记倒计时次数,排队队伍放等着的线程,你只需要定义‘怎么减次数’‘怎么通知队伍开工’。”
简单说:AQS 是个 “懒老板”,只搭架子(状态 + 队列),具体规则让手下(ReentrantLock等)自己填 —— 这就是设计模式里的 “模板方法模式”,AQS 写死流程,子类重写细节。
二、AQS 的核心家当:俩宝贝管全工地
包工头要管好工人,得有俩东西:记名额的小本本(状态变量)和排队的队伍(CLH 队列)。AQS 也一样,核心就靠这俩:
1. 状态变量(state):工地 “通行证” 数量
state是个被volatile修饰的int变量,相当于工地的 “通行证” 总数 —— 具体这 “通行证” 是啥,全看手下怎么定义:
- 要是ReentrantLock当小组长:state就是 “单人通行证” 数量,0 表示没人拿,1 表示有人拿(独占模式),要是同一个工人再来拿,就把state改成 2(可重入,相当于 “续期”)。
- 要是CountDownLatch当小组长:state就是 “开工倒计时”,比如初始设为 5,每完成一项准备工作就减 1,直到 0 表示 “通行证不限量,所有人可以进”(共享模式)。
- 要是Semaphore当小组长:state就是 “多人通行证” 数量,比如设为 3,意思是最多 3 个工人同时进工地(共享模式)。
工人要拿通行证,不能直接抢,得用 “CAS” 操作 —— 相当于工人问包工头:“现在通行证是 x 个不?是我就拿 1 个!” 包工头确认没错才给,保证同时只有一个工人能改 “小本本”,这就是 “原子操作”。
2. CLH 队列:工人排队的 “蛇形队伍”
要是通行证不够了,工人总不能直接抢吧?AQS 就搞了个 “CLH 队列”,让没拿到证的工人排好队 —— 这队列是个双向链表,每个工人是个Node节点,节点里记着:
- 自己是谁(thread);
- 状态(waitStatus):比如 “等着被叫”(SIGNAL,-1)、“不想等了”(CANCELLED,1);
- 前后队友(prev/next):防止排错队。
这队列是 “FIFO”(先进先出)的,相当于工地的 “蛇形通道”,先来的站前面,后来的排后面 —— 但有个小细节:AQS 的队列头节点是个 “哨兵”(空节点),真正的工人从第二个节点开始排,就像排队时第一个位置留着 “放牌子”,方便喊下一个。
三、AQS 的干活流程:抢证→排队→下班
工人进工地的全流程,AQS 早定死了 —— 不管是独占模式(一个人拿证)还是共享模式(一群人拿证),核心都是 “抢证→排队→还证→喊人”:
咱们拿 “独占模式”(比如ReentrantLock)举例,看看工人(线程)怎么干活:
1. 抢证:acquire()方法
工人甲来工地,先调用acquire(1)(要 1 个通行证):
- 第一步:问小组长(ReentrantLock):“我能拿证不?”(调用tryAcquire(1))。
-
- 要是state是 0(没人拿),工人甲用 CAS 把state改成 1,拿证成功,直接进工地干活。
-
- 要是state是 1,但拿证的是自己(重入),就把state改成 2,也进工地。
- 第二步:要是抢不到证(比如state是 1 且不是自己拿的),AQS 就把工人甲包装成Node,塞进 CLH 队列尾巴(用 CAS 保证插队失败,只能排最后)。
- 第三步:工人甲进队后,不是傻等,而是先 “自旋” 几下(问前队友:“你快完事了不?”),要是前队友快好了,再抢一次;要是还没好,就用LockSupport.park()把自己 “暂停”(阻塞),等着被喊醒。
2. 还证:release()方法
工人甲干完活,要还证,调用release(1):
- 第一步:问小组长(ReentrantLock):“我能还证不?”(调用tryRelease(1))。
-
- 要是state是 2(重入了一次),就改成 1(还没还完,相当于 “续期取消一次”);
-
- 要是state是 1,就改成 0(彻底还完),并把 “当前持证人” 设为null。
- 第二步:还完证后,AQS 去队列里找 “头节点的下一个工人”(比如工人乙),用LockSupport.unpark(工人乙)把他喊醒。
- 第三步:工人乙被喊醒后,回到第一步,重新抢证(这时候state是 0,抢证成功),然后把自己变成新的 “哨兵头节点”,原来的头节点被垃圾回收 —— 队列就往前挪了一位。
四、AQS 的两种 “工地模式”:独占 vs 共享
AQS 管工地有俩模式,全看 “通行证” 是 “单人票” 还是 “团体票”:
1. 独占模式:“一人包场”
相当于工地只发 1 张 “单人通行证”,谁拿到谁干活,其他人排队 —— 典型代表是ReentrantLock:
- 工人甲拿证后,工人乙、丙只能排队;
- 甲还证后,只喊队列里的下一个(乙)来拿证。
这里还有个小插曲:ReentrantLock支持 “公平” 和 “非公平”—— 公平就是严格按排队顺序喊人,非公平就是工人刚到工地,不等排队就先问:“哎?刚好没人拿证,我能插个队不?” (默认是非公平,因为插队能少排队,性能更高,就像食堂打饭有人插空,但前提是前面没人)。
2. 共享模式:“多人拼场”
相当于工地发 “团体票”,只要没发完,工人就能直接进,用完了再排队 —— 典型代表是CountDownLatch和Semaphore:
- 比如CountDownLatch设state=3:一开始没人能进,每减 1 次state,就看看是不是到 0 了,到 0 了就喊 “所有排队的工人,都进来!”(共享模式下,唤醒会 “连锁反应”,一个喊一个);
- 比如Semaphore设state=2:前两个工人直接进,第三个工人排队,等其中一个还证(state变回 2),就喊队列里的下一个进来。
五、AQS 的 “手下们”:那些你熟悉的并发工具
AQS 自己是个抽象类,不能直接用,全靠手下 “打工”——JUC 里的同步工具,基本都是 AQS 的 “打工人”:
|
工具类 |
模式 |
用 AQS 干了啥? |
|
ReentrantLock |
独占 |
state记重入次数,tryAcquire实现公平 / 非公平抢票,tryRelease减次数到 0 还票。 |
|
CountDownLatch |
共享 |
state记倒计时,countDown()减state,await()等state到 0 再进。 |
|
Semaphore |
共享 |
state记许可数,acquire()拿 1 个许可,release()还 1 个许可。 |
|
ReentrantReadWriteLock |
读写分离 |
高 16 位记读锁(共享),低 16 位记写锁(独占),读锁能多线程拿,写锁只能一个拿。 |
简单说:这些工具本质上都是 “给 AQS 填规则的打工人”,AQS 搭好架子,它们负责写 “抢票须知” 和 “还票流程”。
六、总结:AQS 是个 “懂管理的懒老板”
聊了这么多,你会发现 AQS 的核心逻辑特别简单:
- 用state记 “资源名额”,用 CLH 队列管 “排队的人”;
- 子类只需要告诉它 “怎么拿名额”(tryAcquire等)和 “怎么还名额”(tryRelease等);
- 不管是锁还是倒计时器,换汤不换药,全靠这俩宝贝和一套流程。
这就是 AQS 的厉害之处:把并发的 “共性问题”(状态管理、线程排队)抽出来做成模板,让开发者不用重复造轮子 —— 你要做个新的同步工具?不用从头写队列和阻塞逻辑,直接继承 AQS,重写几个钩子方法就行。
更多推荐


所有评论(0)