Java AQS(AbstractQueuedSynchronizer)是 JDK 并发包的隐形基石:ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 全都建立在它之上。很多人会用锁,却说不清为什么加锁就能互斥、抢不到锁的线程去哪了。本文从零手写一把可重入锁,把 AQS 的 state、CLH 队列与独占模式彻底讲透,让你以后看任何并发工具源码都不发怵。
一、AQS 的核心三件套:state、队列、CAS
AQS 的设计极其精简,它把”同步状态管理”和”线程排队阻塞”两件事拆开:子类只关心状态怎么变,AQS 负责谁来排队、怎么唤醒。理解下面三件套,就理解了 AQS 的骨架。
1.1 state:一个被 volatile 保护的 int
state 是 AQS 唯一关心的”资源数量”。对独占锁来说,0 表示未被占用、1 表示被占用;对信号量来说,它表示剩余许可数。读它用 getState(),改它必须用 CAS,不能普通赋值。
// AQS 内部关键字段(简化)
private volatile int state;
protected final int getState() { return state; }
protected final void setState(int newState) { state = newState; }
// 原子 CAS 改 state,这是 AQS 并发安全的根基
protected final boolean compareAndSetState(int expect, int update) {
return U.compareAndSwapInt(this, STATE_OFFSET, expect, update);
}
1.2 等待队列:CLH 队列的变体
抢不到锁的线程不会忙等,而是被包装成节点(Node)进入一条双向 FIFO 队列(AQS 借鉴了 CLH 自旋锁的思路,但改成阻塞等待)。队首是”持有锁或正在尝试获取”的节点,其余节点排队并 park 挂起,等前驱释放后被唤醒。
1.3 模板方法:AQS 搭台,子类唱戏
AQS 用模板方法模式把”获取/释放”的核心钩子留给子类实现,自己实现排队、入队、阻塞、唤醒。下面的表说清了分工:
| 类别 | 方法 | 谁来实现 / 提供 |
| 需要子类实现 | tryAcquire / tryRelease(独占) | 你(自定义同步器) |
| 需要子类实现 | tryAcquireShared / tryReleaseShared(共享) | 你 |
| 需要子类实现 | isHeldExclusively | 你 |
| AQS 提供 | acquire / release / acquireShared | 框架(含排队与 park) |
| AQS 提供 | getState / setState / compareAndSetState | 框架 |
二、动手:用 AQS 写一把可重入锁
理论说完直接上代码。我们继承 AbstractQueuedSynchronizer,实现一个支持可重入的独占锁:同一个线程可以多次加锁,必须对应次数释放。
2.1 自定义 Sync
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
class ReentrantLockSync extends AbstractQueuedSynchronizer {
// 是否处于独占(被锁住)状态
@Override
protected boolean isHeldExclusively() {
return getState() > 0;
}
// 尝试获取:state 0 -> 1(首次),或当前线程已持有则重入 +1
@Override
protected boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current); // 记录持有线程
return true;
}
} else if (getExclusiveOwnerThread() == current) {
int next = c + acquires; // 重入计数 +1
if (next < 0) throw new Error("超出最大重入次数");
setState(next);
return true;
}
return false; // 被别的线程持有,入队等待
}
// 尝试释放:重入计数 -1,归零才真正释放
@Override
protected boolean tryRelease(int releases) {
int c = getState() - releases;
if (getExclusiveOwnerThread() != Thread.currentThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free; // 只有完全释放才唤醒后继
}
}
关键点有两个:① 用 setExclusiveOwnerThread 记录持有者,这是可重入判断的依据;② tryRelease 只有计数归零才返回 true,否则只是把重入层数减一,锁依然被持有。
2.2 封装成 Lock 接口
import java.util.concurrent.locks.Lock;
public class MyReentrantLock implements Lock {
private final ReentrantLockSync sync = new ReentrantLockSync();
@Override public void lock() { sync.acquire(1); }
@Override public void unlock() { sync.release(1); }
@Override public boolean tryLock() { return sync.tryAcquire(1); }
// 为简洁省略 interruptibly / 超时 / Condition 实现
@Override public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override public Condition newCondition() { return sync.newCondition(); }
@Override public boolean tryLock(long t, TimeUnit u) throws InterruptedException {
return sync.tryAcquireNanos(1, u.toNanos(t));
}
}
注意 lock() 调的是 AQS 的 acquire(1) 而不是 tryAcquire:当 tryAcquire 返回 false 时,AQS 会自动把线程入队、park 挂起,等锁释放再唤醒重试。排队逻辑我们一行都没写,全交给框架。
2.3 并发自测
public static void main(String[] args) throws InterruptedException {
MyReentrantLock lock = new MyReentrantLock();
int[] counter = {0};
int threads = 10, per = 1000;
List<Thread> ts = new ArrayList<>();
for (int i = 0; i < threads; i++) {
ts.add(new Thread(() -> {
for (int j = 0; j < per; j++) {
lock.lock();
try { counter[0]++; } // 临界区
finally { lock.unlock(); }
}
}));
}
ts.forEach(Thread::start);
for (Thread t : ts) t.join();
System.out.println("期望=" + (threads * per) + " 实际=" + counter[0]);
// 输出:期望=10000 实际=10000 —— 锁保证了原子性
}
三、独占模式 vs 共享模式
AQS 同时支持两种资源语义。独占就像独卫:一个人用,别人必须等;共享像公共卫生间有 N 个坑位:state 表示剩余坑位数,多个线程可同时进入,直到耗尽。
| 维度 | 独占模式 | 共享模式 |
| 子类钩子 | tryAcquire / tryRelease | tryAcquireShared / tryReleaseShared |
| state 含义 | 0 空闲 / ≥1 被占 | 剩余许可数(可为负) |
| 典型实现 | ReentrantLock | Semaphore、CountDownLatch、读锁 |
| 释放后 | 只唤醒一个后继 | 可能连锁唤醒整条队列 |
3.1 Semaphore 如何用共享模式
// Semaphore 内部 Sync 的简化逻辑(共享模式)
protected int tryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining; // 负数表示获取失败,将排队
}
}
返回负数代表”资源不够,去排队”;返回非负数代表”拿到了,还能唤醒后面同样能拿到的人”。这就是共享模式会连锁传播唤醒的原因。
四、公平锁与非公平锁
ReentrantLock 默认非公平:新来的线程可以插队抢锁,性能好但可能让排队线程”饿死”。构造时传 true 则变公平:必须先看队列里有没有前驱在等。
| 对比 | 非公平(默认) | 公平 |
| 抢锁策略 | 直接 CAS 抢,失败再排队 | 先 hasQueuedPredecessors() 判断 |
| 吞吐 | 高(减少上下文切换) | 较低(严格排队) |
| 饥饿风险 | 有 | 无 |
// 公平锁在 tryAcquire 里多一道检查
protected boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 关键:队列里还有人在等,就别插队
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (getExclusiveOwnerThread() == current) {
setState(c + acquires);
return true;
}
return false;
}
五、AQS 的阻塞与唤醒:park / unpark
队列里的线程不是自旋烧 CPU,而是用 LockSupport.park() 挂起,释放锁时用 unpark() 精准唤醒后继节点。相比 synchronized 的监视器,AQS 的唤醒粒度更细——只唤醒队列头部的下一个有效节点,而不是随机唤醒一个。
// AQS 内部(极度简化)的唤醒逻辑
private void unparkSuccessor(Node node) {
Node s = node.next;
if (s == null || s.waitStatus > 0) { // 跳过已取消节点
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0) s = t;
}
if (s != null)
LockSupport.unpark(s.thread); // 只唤醒一个
}
六、常见误区与排查清单
| 误区 / 坑 | 正确做法 |
用 setState 而非 CAS 改 state | 多线程下必须用 compareAndSetState |
| tryRelease 不判断持有线程 | 非持有线程释放要抛 IllegalMonitorStateException |
| 重入计数不配套 | 加几次锁就要对应释放几次,否则死锁或泄漏 |
| lock 后临界区抛异常 | 必须在 finally 里 unlock() |
| 共享模式忘记传播 | 返回值符号决定要不要连锁唤醒后继 |
七、AQS 与虚拟线程的关系
很多人担心:Java 21 的Java 21 虚拟线程出来后,还要不要学 synchronized 和 AQS?答案是更要学。虚拟线程在 synchronized 块或原生 ReentrantLock 上会”钉住”(pin)载体线程,反而要求你更懂锁的粒度。理解 AQS 能帮你判断哪些临界区该用 ReentrantLock、哪些该拆小,避免把虚拟线程的优势耗在锁竞争上。相关的高并发范式演进也可以看虚拟线程(Project Loom)实战一文,以及强调不可变思想的Java 函数式编程。
八、落地清单
- 需要互斥且要可重入、可超时、可中断 → 优先用现成
ReentrantLock,别重复造轮子。 - 要限制并发数(如限流、连接池)→ 用
Semaphore(共享模式)。 - 要等一组任务完成再继续 → 用
CountDownLatch/CyclicBarrier。 - 确实需要自定义同步语义(如读写分离、状态机)→ 再继承 AQS 实现
tryAcquire/tryRelease。 - 无论用哪种锁,
unlock()必须放在finally;本地跑并发用例建议在容器或多核环境验证,并在 CI 里加并发测试。
九、小结
Java AQS 用”state + CLH 队列 + CAS”三件套,把并发同步的复杂度收敛到一个可扩展的骨架里。你写的不过几十行 tryAcquire/tryRelease,排队、阻塞、唤醒全由框架兜底。吃透它,不仅是能手写一把锁,更是拿到了阅读整个 JUC 并发包源码的钥匙。下回遇到诡异的并发 bug,你也能从”锁状态”和”等待队列”两个角度精准定位。




