Java AQS(AbstractQueuedSynchronizer,抽象队列同步器)是 JDK 并发包 java.util.concurrent.locks 的底层基石。你日常使用的 ReentrantLock、Semaphore、CountDownLatch 全都构建在它之上。很多开发者只会调用 lock()/unlock(),却说不清”拿不到锁的线程去哪了”。本文不堆术语,而是带你用约 150 行代码手写一个可重入锁,把 AQS 的状态机与等待队列彻底讲透。理解 AQS 之后,再看 Java 虚拟线程的协作式调度 与 synchronized 的锁升级 会更通透。
一、为什么需要 AQS
在 AQS 出现之前,Java 的同步原语主要靠 synchronized 关键字和 Object 的 wait()/notify()。它们能用,但有硬伤:无法中断地等待、无法超时、无法感知”公平还是非公平”、也无法方便地实现”多个线程同时通过”(比如信号量)。Doug Lea 在 JUC(java.util.concurrent)中抽象出 AQS,把”线程排队、阻塞、唤醒”这一套复杂逻辑收敛到一处,让上层的同步器只需关心自己的一套规则。这正是模板方法模式的经典应用。
AQS 只暴露几个需要子类实现的钩子方法(hook),其余的队列管理、线程挂起与唤醒全部由父类完成。换句话说:你定义”能不能拿锁”,AQS 负责”拿不到怎么办”。这种分工让 90% 的并发同步逻辑被复用,也大幅降低了出错概率。
二、AQS 核心思想:一个状态 + 一个队列
2.1 state:同步状态
AQS 内部维护一个 volatile int state,它代表”资源”的占用情况。语义由子类自由定义:在互斥锁里,state=0 表示无人持有,state=1 表示被占用;在可重入锁里,state>0 还记录了重入次数;在信号量里,state 表示剩余许可数量。对 state 的修改必须用 CAS(compareAndSetState),保证原子性。
2.2 CLH 队列:等待线程的双向链表
当线程抢锁失败时,AQS 会把它包装成一个 Node 节点,加入一条名为 CLH(Craig-Landin-Hagersten)的双向等待队列。队列头节点是”已持有锁”的虚拟节点,后续节点按 FIFO 顺序排队。每个节点有一个 waitStatus,标识它是否需要被唤醒。线程入队后通过 LockSupport.park() 挂起,避免空转消耗 CPU——这与 线程池的任务队列思路 异曲同工,都是”排队 + 等待调度”。
2.3 独占模式 vs 共享模式
AQS 支持两种资源获取方式:独占(Exclusive)如锁,同一时刻只有一个线程能持有;共享(Shared)如读锁或信号量,多个线程可同时持有。子类通过重写 tryAcquire/tryRelease(独占)或 tryAcquireShared/tryReleaseShared(共享)来声明自己的规则。
| 钩子方法 | 模式 | 你需要返回什么 |
| tryAcquire(arg) | 独占 | true=获取成功,false=失败入队 |
| tryRelease(arg) | 独占 | true=释放后应唤醒后继 |
| tryAcquireShared(arg) | 共享 | 正数=还有余量,0=刚好,负数=失败 |
| tryReleaseShared(arg) | 共享 | true=释放后应传播唤醒 |
| isHeldExclusively() | 独占 | 当前线程是否独占资源 |
三、实战:用 AQS 手写一个可重入锁
3.1 先搭骨架:继承与钩子
最简单的可重入锁,只需重写 tryAcquire 和 tryRelease。下面先给出骨架,再补可重入细节。
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class ReentrantLockByAqs {
private final Sync sync = new Sync();
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
// 待实现:CAS 抢 state
return false;
}
@Override
protected boolean tryRelease(int arg) {
// 待实现:释放并清 owner
return false;
}
@Override
protected boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
}
public void lock() { sync.acquire(1); }
public void unlock() { sync.release(1); }
}
3.2 可重入的关键:记录持有线程与计数
可重入意味着”同一个线程多次加锁不会把自己锁死”。实现要点有两条:①用 getExclusiveOwnerThread() 记录当前持锁线程;②用 state 记录重入次数。第一次获取把 owner 设为自己、state 置 1;重入时仅 state+1;释放时 state-1,减到 0 才真正清 owner 并允许唤醒后继。
3.3 完整实现
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class ReentrantLockByAqs {
private final Sync sync = new Sync();
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 无人持有:CAS 抢占,并把 owner 设为当前线程
if (compareAndSetState(0, arg)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (getExclusiveOwnerThread() == current) {
// 已是自己持有:重入,计数累加(注意 int 溢出)
int next = c + arg;
if (next < 0) throw new Error("Maximum lock count exceeded");
setState(next);
return true;
}
return false; // 被别人持有,入队等待
}
@Override
protected boolean tryRelease(int arg) {
int c = getState() - arg;
if (getExclusiveOwnerThread() != Thread.currentThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null); // 真正释放
}
setState(c);
return free; // 仅当 free=true 时 AQS 才唤醒后继
}
@Override
protected boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
}
public void lock() { sync.acquire(1); }
public void unlock() { sync.release(1); }
}
注意 tryRelease 返回的 free:只有 state 归零才返回 true,AQS 才会触发后继节点的 unpark。重入一次就 unlock 一次、但没释放干净时,锁依然被持有——这正是可重入锁"加几次就得解几次"的本质。
四、公平锁与非公平锁
上面的实现其实是非公平的:新来的线程会直接 CAS 抢锁,即便队列里已有排队的线程。这也是 ReentrantLock 默认非公平的原因——吞吐更高。若想要公平(先来先得),tryAcquire 里加一道闸门:队列里有前驱在等,就别抢。
// 公平版 tryAcquire 的差异片段
if (c == 0) {
// hasQueuedPredecessors():队列中有更早的等待者就放弃抢占
if (!hasQueuedPredecessors() && compareAndSetState(0, arg)) {
setExclusiveOwnerThread(current);
return true;
}
}
| 维度 | 非公平锁 | 公平锁 |
| 抢占顺序 | 后来居上,可能插队 | 严格 FIFO |
| 吞吐量 | 高(减少上下文切换) | 略低 |
| 饥饿风险 | 极端下有线程长期拿不到 | 无 |
| 典型用途 | 通用默认 | 需保证顺序的场景 |
五、AQS 在 JDK 中的身影
学会手写之后,你会发现 JDK 里的同步器只是一个个 AQS 的"规则实现":
- ReentrantLock:独占模式,state 即重入计数,公平/非公平两套
Sync内部类。 - Semaphore:共享模式,state 即剩余许可数,
acquire扣减、release归还。 - CountDownLatch:共享模式,state 即计数器,归零后所有等待线程被一次性唤醒。
- ReentrantReadWriteLock:把一个 int 的 state 拆成高 16 位(读计数)和低 16 位(写计数),玩转位运算。
理解了这套骨架,当你需要定制同步语义(如"限流闸门""批量许可"),就能像 分布式锁的设计 那样,从底层重新组合规则,而不是硬套现成 API。
六、常见坑与排障
| 坑 | 现象 | 修复 |
| 重入计数与释放不匹配 | 锁永远不释放 / 提前释放 | 加几次锁必须解几次,state 归零才清 owner |
| 在 tryAcquire 里阻塞 | 线程卡死、队列错乱 | 钩子方法只能 CAS 判断,不能 park |
| 忘记 setExclusiveOwnerThread | isHeldExclusively 与重入失效 | 获取成功立即设置,释放归零立即清空 |
| 用 int 计数溢出 | 负数 state 引发异常 | 累加前判断 next < 0 抛错 |
七、落地建议
生产环境绝大多数场景直接用 ReentrantLock 即可,不必重复造轮子;但读懂 AQS 的价值在于"遇事不慌":当你的锁出现死锁、线程堆积、唤醒不及时时,能顺着 state、CLH 队列、park/unpark 这条链路精准定位。建议在本地把本文的示例代码跑通,再用 jstack 观察不同状态下线程的 BLOCKED / WAITING 栈帧,把"队列""挂起""唤醒"从概念变成你脑中的动态画面。配合 Java NIO 零拷贝 这类底层文章一起读,你对"Java 如何与操作系统协作"的认知会更完整。




