终于知道aqs是啥东西了
java
Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区
} finally {
lock.unlock();
}
AQS 是 AbstractQueuedSynchronizer 的缩写,藏在 java.util.concurrent.locks 包下面。它不是锁,不是工具类,是造锁用的模板------子类只负责回答一个问题:"我现在能抢到吗?",剩下的排队、入队、阻塞、唤醒全交给 AQS。
从 lock.lock() 走进 AQS

AQS 到底是什么
AQS 抽象类提供四个共享部件:
- 一个
volatile int state。空闲为 0,被占用为 1,重入一次就 +1;Semaphore 把 state 翻译成剩余许可证,CountDownLatch 把它翻译成剩余倒计数。所有同步语义都折算成对这一个字段的 CAS 操作。 - 一组基于
Unsafe.compareAndSwapInt的 CAS,覆盖抢锁、释放、入队、状态翻转。 - 一条 FIFO 同步队列(双向链表),抢锁失败的线程挂在这里。
- 一对
LockSupport.park/unpark,挂起和恢复线程,避免空转。
ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 都嵌一个继承 AQS 的 Sync 子类,把上面四件套当基建,自己只实现 tryAcquire / tryRelease(独占)或 tryAcquireShared / tryReleaseShared(共享)这几个钩子。骨架是 AQS 的,语义是子类的。
AQS 的四个核心部分

一次抢锁失败的完整过程
线程 A 先到,compareAndSetState(0, 1) 成功,setExclusiveOwnerThread(Thread.currentThread()) 把"我"登记成独占者,tryAcquire 返回 true,acquire 模板结束。
线程 B 接着到。CAS 失败,tryAcquire 返回 false,acquire 模板接管:
线程获取锁失败后的流程

java
// AQS 提供的模板方法(简化版,便于理解流程,不是逐行源码)
public final void acquire(int arg) {
if (!tryAcquire(arg)) { // 1. 再试一次
Node node = addWaiter(); // 2. 入队
acquireQueued(node, arg); // 3. 在队列里 park
}
}
public final boolean release(int arg) {
if (tryRelease(arg)) { // 1. 释放 state
Node h = head;
if (h != null && h.waitStatus != 0) {
unparkSuccessor(h); // 2. 唤醒下一个节点
}
return true;
}
return false;
}
// 子类必须实现的钩子(Mutex 风格简化版:不可重入)
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, arg)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int arg) {
if (getState() == 0 || getExclusiveOwnerThread() != Thread.currentThread()) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
// 非重入 Mutex 直接把 state 清成 0;ReentrantLock 这里会做 c-1,
// 因为重入时 state 是大于 1 的计数,必须逐层释放。
setState(0);
return true;
}
AQS 为 B 创建一个 Node 放到队尾,acquireQueued 让 B 在队列里循环:
- 如果自己是 head 的后继,再 CAS 抢一次------给已经在排队的线程一个窗口,万一 A 刚好释放锁就不用 park。
- 不是第二个或者再抢又败,把前驱的
waitStatus改成SIGNAL("你释放的时候记得叫我")。不设 SIGNAL 的话 park 起来再也没人叫醒。 - 调
LockSupport.park(this)挂起当前线程,停在这里不再消耗 CPU。
LockSupport.park 也可能在没有任何 unpark 或 interrupt 的情况下自己返回,即通常说的伪唤醒(spurious wakeup)。防御原则和 Object.wait / Condition.await 完全一致:调用方必须在循环里重新检查谓词(这里是 tryAcquire),不能把 park 返回当成事件已经发生。acquireQueued 的 for (;;) 正是这个防御循环------park 一返回就重新 tryAcquire,抢不到就再次 park,既兜底正常唤醒也兜底伪唤醒。
线程 A 干完活调 unlock(),走 release(1)。tryRelease 把 state 改回 0,清掉独占者,返回 true。release 拿到 true 之后查的是 head.waitStatus------也就是 head 节点自己的状态字段,不是 head.next。如果它不是 0(通常是 SIGNAL),调 unparkSuccessor(head):先读 head.next;如果 head.next 不为 null 且 waitStatus <= 0,就直接用它;否则从 tail 沿着 prev 反向扫描,保留离 head 最近、且 waitStatus <= 0 的后继节点,再对它做 LockSupport.unpark。这是因为 head.next 可能为 null(队列里只剩 head),或者后继已经被 cancel(waitStatus > 0),此时必须反向兜底找到下一个可唤醒的人。B 在 acquireQueued 里重跑 tryAcquire------这次 CAS 成功,state 变 1,进入临界区。
state 到底代表什么
state 是 volatile int,不同子类把同一个 int 翻译成不同业务:
| 同步器 | state 语义 | 加减规则 |
|---|---|---|
ReentrantLock |
重入次数,0 表示空闲;公平性由 tryAcquire 是否先查队列决定 |
获取时 +1,释放时 -1 |
Semaphore |
剩余许可证数量 | acquire 时 -1,release 时 +1 |
CountDownLatch |
剩余计数 | countDown 时 -1,到 0 时唤醒所有等待者 |
ReentrantReadWriteLock |
高 16 位为读锁计数,低 16 位为写锁计数 | 读锁 +1<<16,写锁 +1 |
看到 getState() 的返回值就能判断锁处于什么状态:Semaphore 的 state = 3 表示还能并发放行 3 个;CountDownLatch 的 state = 0 表示 latch 已打开,所有 await 都已返回。
ReentrantReadWriteLock 的 state 等于"读锁数×65536 + 写锁数(0 或 1)",看一次 getState() 就同时知道读了几把、写了几把。
同步队列怎么工作
同步队列是一条带头结点的双向链表,由 head 和 tail 两个指针维护。头节点是"虚节点",不绑定线程,只是稳定入口,后继才是真正第一个等锁的线程。抢到锁的线程进入临界区之前会调 setHead(node):把 head = node,把 node.thread 和 node.prev 清成 null;调用方随后再做 p.next = null 把旧前驱节点的 next 指针也断开,让原节点可以被 GC 回收。注意,每次新的抢锁失败仍然会通过 addWaiter 分配一个新的 Node,setHead 不构成对象池------它的作用只是把已经持有锁的节点从队列语义里"摘出去",不再阻塞后面的唤醒路径。
每个 Node 关键字段:
thread:当前节点代表的线程;取消后置为 null。waitStatus:volatile int,节点刚插入同步队列时是 0;之后在 0、SIGNAL(-1,后继需要被唤醒)、CANCELLED(1,被取消)、CONDITION(-2,节点在条件队列上)、PROPAGATE(-3,共享模式下传递唤醒)这几个值之间切换,对应节点生命周期的不同阶段。CANCELLED 是终态,被其他线程 park 前顺手清理;其他三个状态是流转中的中间值。prev/next:前后指针,新节点入队时通过 CAS 改tail。nextWaiter:同步队列的顺序仍由prev/next维护,nextWaiter不参与同步队列排序。它只做两件事------把同一张条件队列上的节点串成单链表(Condition.await 用它串条件队列);给当前节点打上模式标记,独占节点对应Node.EXCLUSIVE(值为 null),共享节点对应Node.SHARED(值为一个new Node()哨兵)。
AQS 同步队列
入队从 tail 加,避免多线程争抢时链表被搞乱;出队从 head 的后继节点开始唤醒。CANCELLED 节点在 shouldParkAfterFailedAcquire 阶段被跳过并清理,清理是惰性的------没有专门清扫线程,是某个线程 park 之前顺手把前面已取消的节点跳掉。
prev 和 next 都不可少:取消节点时需要把前驱和后继直接连起来跳过自己,只有 next 的话删除要从头遍历,O(n) 不可接受,双向链表把删除降到 O(1)。
park 比自旋节省 CPU:while (true) 空转会吃满一个核心,多线程下自旋也抢不到锁,反而浪费调度。LockSupport.park 底层走 Unsafe.park,最终调用操作系统的阻塞原语(比如 Linux 的 futex),把线程从运行队列移到等待队列,让出 CPU。park 之前 acquireQueued 会再做一次 tryAcquire------这是为了避免多线程竞争时无意义的 park / unpark 往返,比如刚 park 起来就被 unpark,浪费一次上下文切换;自旋没用,但 park 之前再试一次 CAS 是有意义的。
unpark 之后线程要重新进入 acquireQueued 的循环再次 tryAcquire。这个窗口期里另一个线程可能已经把锁抢走------前头插进来一个非公平模式的 ReentrantLock 新线程是常见情况。acquireQueued 的 for (;;) 循环就是为此兜底的:每次 park 返回都重新检查谓词并重试,"被唤醒"和"拿到锁"之间永远隔一次重试。
独占模式和共享模式
AQS 把"如何获取同步状态"分成两种语义,对应两个不同的钩子:
- 独占模式(exclusive) :一次只允许一个线程通过,
tryAcquire返回 boolean。ReentrantLock、ReentrantReadWriteLock.WriteLock都是独占,钩子方法是tryAcquire/tryRelease。 - 共享模式(shared) :一次允许多个线程通过,
tryAcquireShared返回int:负数代表失败,0 代表成功但后续不再放行,正数代表剩余可用资源。Semaphore、CountDownLatch、ReentrantReadWriteLock.ReadLock都是共享,钩子方法是tryAcquireShared/tryReleaseShared。
共享模式 tryAcquireShared 返回 int,必须告诉调用方"我现在还剩多少配额":doAcquireShared 根据返回值判断要不要继续唤醒后面的节点------这就是 PROPAGATE 机制。Semaphore 有 5 个许可证时,tryAcquireShared 在第一个通过时返回 4,第二个返回 3,依此类推;返回正数,AQS 就知道还有余量,把后继一并唤醒让他们一起去抢。如果自定义同步器把 tryAcquireShared 写成返回 boolean,信号传播就丢了,许可证还有剩余但后面的线程一直在 park。
PROPAGATE 这个状态是为共享模式单独设计的,对应 doReleaseShared 的第二条分支。doReleaseShared 在循环里做两件事:
- 如果 head 当前
waitStatus == SIGNAL:CAS 把 SIGNAL→0,CAS 失败就 retry;成功之后再走unparkSuccessor(head)。 - 如果 head 当前
waitStatus == 0:CAS 把 0→PROPAGATE,CAS 失败也 retry;这一步本身不 unpark,只是给 head 打一个传播位。
PROPAGATE 的语义是"下一次 release 之前,如果下游 acquireShared 还没来得及看到 release 信号,可以通过 PROPAGATE 把它传播下去"------它处理的是连续 releaseShared 之间 head 状态被快速翻转、或者共享模式的 acquireShared 与 release 交错时的边界情况,保证下游唤醒不丢。它不是"release 时当前没在拿锁就触发"的语义,是 head 状态机的兜底传播位。
ReentrantReadWriteLock 把两种模式叠在一起用:读锁是共享、写锁是独占,AQS 通过 tryAcquireShared 和 tryAcquire 两套钩子同时支持。一旦写锁被持有,读锁的 tryAcquireShared 返回负数,所有新读者 park;写锁释放后读锁放行,又触发 PROPAGATE 把等待中的读者一次性叫醒一批。
用 AQS 实现 Mutex
不靠 ReentrantLock,直接用 AQS 写一个非重入互斥锁 Mutex,对应 aqs-blog/examples/Mutex.java:
java
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public final class Mutex implements Lock {
private static final class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
if (arg != 1) {
throw new IllegalArgumentException("arg must be 1, was: " + arg);
}
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
if (arg != 1) {
throw new IllegalArgumentException("arg must be 1, was: " + arg);
}
if (getState() == 0 || getExclusiveOwnerThread() != Thread.currentThread()) {
throw new IllegalMonitorStateException(
"release attempted by non-owner or on an unlocked mutex");
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1 && getExclusiveOwnerThread() == Thread.currentThread();
}
Condition createCondition() {
return new ConditionObject();
}
}
private final Sync sync = new Sync();
@Override public void lock() { sync.acquire(1); }
@Override public void unlock() { sync.release(1); }
@Override public boolean tryLock() { return sync.tryAcquire(1); }
@Override public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override public Condition newCondition() { return sync.createCondition(); }
}
Mutex 只回答一个问题:state 是不是 0。CAS 成功就拿锁,失败就让 AQS 去排队------排队、入队、park、唤醒、重试,全部由 AQS 父类负责。tryRelease 严格校验"是不是当前线程持有",避免别的线程误释放;isHeldExclusively 给 Condition 用,确保只有持有锁的线程才能调 await;newCondition() 返回 AQS 内部提供的 ConditionObject。lockInterruptibly 和带超时的 tryLock(long, TimeUnit) 分别走 acquireInterruptibly 和 tryAcquireNanos,前者响应中断,后者自带超时返回 false------这是 AQS 提供的三套获取语义:阻塞不可中断、阻塞可中断、阻塞限时。
用这个 Mutex 包住一个计数器:
java
public final class SafeCounter {
private final Mutex lock = new Mutex();
private int value;
public void increment() {
lock.lock();
try {
value++;
} finally {
lock.unlock();
}
}
public int get() {
lock.lock();
try {
return value;
} finally {
lock.unlock();
}
}
}
MutexDemo 用 8 个线程 × 10000 次自增跑一遍:
text
expected = 80000
actual = 80000
几十行钩子代码 + 一个 int,就拿到一套和 ReentrantLock 同款的排队机制。Mutex 比 ReentrantLock 简单是因为它不做重入也不分公平/非公平,把这两块补回来就是完整 ReentrantLock。isHeldExclusively 这个钩子在 Condition 路径里被 AQS 调用,用来在 ConditionObject.signal() / signalAll() 之前断言当前线程持有锁;await 路径上的锁所有权不是靠它保证,而是走 fullyRelease(node) → release() → 子类的 tryRelease(),非持有者调用 await 会在 tryRelease 那里抛 IllegalMonitorStateException。Mutex 这里只判断 state == 1 && 独占线程 == 当前线程,等效于 ReentrantLock.getHoldCount() > 0 的轻量版。
Condition、公平性和常见误区
Condition 必须从 AbstractQueuedSynchronizer.ConditionObject 创建,本质上是另一条链表------条件队列。它和同步队列共享 state,但每个 ConditionObject 自己维护一张条件队列,互不干扰。
当线程在锁内调用 condition.await() 时:
Condition 节点转移

- 当前线程释放锁(相当于走一遍
release),state 回到可用状态。不释放的话别的线程就拿不到锁,永远没人signal我们,就会死锁。 - AQS 新建一个条件队列上的 Node (
waitStatus = CONDITION,thread指向当前线程),挂到当前 ConditionObject 维护的条件队列尾部。线程之前如果在同步队列里,那个同步队列节点不会被搬过来复用;旧同步节点随后被清理。 - 当前线程在条件队列里 park,进入等待状态。
condition.signal() 从条件队列头部摘一个节点,重新挂回同步队列尾部,等它重新排队抢锁(这一步由 ConditionObject 内部的 CAS 完成)。被 signal 唤醒后这个节点会沿着普通 acquireQueued 路径走:先 tryAcquire,抢不到就 park、再被前驱 unpark。和同步队列的新节点相比,它没有"插队"特权------signal 只是把它送回排队起跑线,最终能不能拿到锁还得看队列里其他线程的状态。如果没人 signal,这个线程会一直 park 下去,哪怕锁已经空出来------这也是 await 必须放在循环里判断条件的原因,不能假设 await 一返回条件就成立了。
公平性的选择在 new ReentrantLock(...) 构造期定型:构造时根据 fair 参数决定装哪一套 Sync------new ReentrantLock(true) 选 FairSync,new ReentrantLock()(默认)选 NonfairSync。两种 Sync 的区别在 tryAcquire:FairSync 在调 compareAndSetState 之前先调 hasQueuedPredecessors() 检查同步队列里是否有先到的线程,有就返回 false 让位,没有才允许 CAS 抢锁;NonfairSync 不查队列直接 CAS,允许新线程插队。
公平模式的语义是"严格按入队顺序分配锁":已经在队列里的线程按到达先后排队,新线程必须先看队列里有没有先到的节点,有就让位。但吞吐量会差,每一次传递都要经历 unpark → 唤醒等待线程 → 它重新 tryAcquire → 上下文切换这条完整链路,CPU 和调度成本比非公平模式高。非公平模式则允许新线程不查队列直接抢------吞吐高,但可能出现饥饿。
synchronized 在 JDK 8 里默认也是非公平。HotSpot 内部用 ObjectMonitor 实现,每个 monitor 自带 _cxq(竞争栈,新来的线程压入栈顶)、_EntryList(park 状态的候选队列)、_WaitSet(Object.wait 的线程链表)这几个内部结构,但它们的入队策略和 AQS 的双向链表不同------_cxq 是栈、_EntryList 抽线程的顺序也不保证 FIFO,所以即便有队列,行为上也不保证严格意义上的先来后到。在单线程反复进入临界区时会触发偏向锁(biased locking)优化,但只要出现第二个线程争抢,偏向就会撤销,恢复到非公平的轻量级 CAS / 重量级 monitor 路径。ReentrantLock 用 AQS 把这套行为拆成可控的 API------fair 参数、newCondition()、tryLock(time)------开发者可以精细控制;synchronized 是 JVM 内置关键字,行为固定、配置项少,但和 AQS 共用同一个 OS 互斥底层(Linux 上都是 futex)。
几个常见误区:
-
"park 等于 sleep" :唤醒条件差别很大。
Thread.sleep只会在超时之后醒来,或者被interrupt中断后通过抛InterruptedException醒来;它不响应unpark,只能靠时间或者中断。LockSupport.park不抛InterruptedException------被unpark直接返回,被interrupt时也是直接返回(标记位留下,调用方自己查Thread.interrupted())。Thread.sleep写不出"等一个事件"的语义,LockSupport.park才是事件等待的工具。 -
"被唤醒就拿到锁了" :唤醒只是回到
acquireQueued重新尝试,可能再次失败再次 park。看到 unpark 不要急着下结论"它拿到锁了"。 -
"AQS 自己就是锁" :AQS 是模板,没有业务语义,必须由子类实现
tryAcquire/tryRelease(独占)或tryAcquireShared/tryReleaseShared(共享)才有意义。直接new AbstractQueuedSynchronizer()没意义,调用 acquire 会因为 tryAcquire 抛 UnsupportedOperationException 而失败。 -
"中断会打断 park" :默认会,但
acquire模板里如果tryAcquire一直失败,park被中断后还会再走一遍循环,直到真的拿到锁。普通的lock()是不可中断的------调thread.interrupt()会记录中断标志位但线程继续 park 等锁;想要可中断地等,得用lockInterruptibly()。Mutex 上面lockInterruptibly/tryLock(time, unit)分别走sync.acquireInterruptibly/sync.tryAcquireNanos:acquireInterruptibly在入口处先调一次Thread.interrupted()检查中断标志位,置位就直接抛InterruptedException;进队列后由parkAndCheckInterrupt()负责------它先 park,park 返回后再查中断标志位,置位就退出循环让上层抛异常。也就是说,中断不是"每次 park 前都查一遍",而是"入口查一次 + 每次 park 返回后查一次"。
读 AQS 源码时容易卡在 shouldParkAfterFailedAcquire 和 parkAndCheckInterrupt 这两个方法上------它们做的事很细,但跳过去只看 acquireQueued 的骨架也能把整条链路跑通。抓住 state、队列、park/unpark 这三件套,剩下的细节都是对它们的补丁。顺带一提,Condition.await 内部也走 park,所以 AQS 的"防御性循环"原则在条件队列里一样适用:signal 之后要重新检查谓词,不能假设条件成立。