AQS原理入门
1. 这是什么
AQS 是 Java 并发包中非常重要的同步框架,很多锁和同步器都建立在它之上。
它的核心作用是管理同步状态和等待队列。
一句话理解:
- AQS 不是某一把具体的锁
- 它更像一个“同步器脚手架”
2. 为什么重要
如果想真正理解这些并发工具为什么能工作,AQS 是绕不开的一层:
ReentrantLockSemaphoreCountDownLatchReentrantReadWriteLock
学会 AQS,你才能把“会用”提升到“看得懂实现思路”。
3. 先建立直觉:AQS 到底在统一什么
很多并发工具表面看起来功能不一样:
- 有的控制互斥
- 有的控制共享访问
- 有的负责等待一批线程完成
但它们内部都面临类似问题:
- 当前能不能获取资源
- 获取失败后线程去哪等
- 什么时候该唤醒等待线程
- 用什么状态表示当前同步条件
AQS 就是在统一处理这些共性问题。
4. 核心内容
4.1 state 是什么
AQS 里最核心的字段之一是 state。
你可以把它理解成:
- 同步状态的数字化表示
不同同步器会用它表达不同含义,例如:
- 锁是否被占用
- 当前重入次数
- 信号量还剩多少许可证
- 闭锁是否已经打开
所以:
- AQS 不规定
state一定表示什么 - 它只提供原子管理这个状态的机制
4.2 独占模式和共享模式
AQS 支持两种核心模式:
独占模式
同一时刻只允许一个线程成功获取资源。
典型代表:
ReentrantLock
共享模式
允许多个线程按某种共享规则同时获取资源。
典型代表:
SemaphoreCountDownLatch
理解这个区别非常重要,因为它决定了:
- 获取成功后的传播行为
- 释放资源后唤醒谁
4.3 等待队列在做什么
当线程获取同步状态失败时,AQS 不会让线程凭空消失。
它会把线程包装成节点,放进一个 CLH 风格的双向等待队列中。
可以先把它想象成:
- 一个排队区
- 拿不到资源的线程先进去等
- 条件满足时再被唤醒尝试获取
所以等待队列本质上解决的是:
- 竞争失败线程如何有序等待和唤醒
4.4 获取与释放流程怎么理解
先用最抽象的方式理解:
获取
- 先尝试直接获取同步状态
- 如果成功,立即返回
- 如果失败,加入等待队列
- 被唤醒后再重试
释放
- 修改
state - 判断是否需要唤醒后继节点
- 通知等待线程继续竞争
这就是很多锁和同步器行为的共同骨架。
4.5 为什么说 AQS 不直接实现业务锁
AQS 本身不会告诉你:
- 锁什么时候算获取成功
- 释放时状态怎么变化
- 共享和独占具体怎么判断
这些都交给子类实现,例如重写:
tryAcquiretryReleasetryAcquireSharedtryReleaseShared
这也是它能复用成多种同步器的根本原因。
4.6 失败重试和阻塞唤醒
AQS 的核心思路不是“失败就结束”,而是:
- 尝试
- 失败入队
- 被唤醒
- 继续尝试
这也是为什么你在源码里会看到:
- 自旋
parkunpark
这些动作组合在一起。
4.7 学 AQS 时最容易踩的坑
很多人第一次看 AQS 容易陷入两个极端:
- 只背源码细节,最后记不住
- 只记“它有个队列和 state”,但不知道怎么落到同步器上
更好的学习顺序是:
- 先理解
state - 再理解独占 / 共享
- 再理解获取失败入队
- 最后结合具体同步器去看源码
5. 学习重点
这一章最重要的是掌握这几个判断:
- AQS 是同步框架,不是具体锁
state是同步状态核心- 队列承载的是竞争失败线程
- 独占模式和共享模式是两套重要语义
- 很多同步器之所以能复用,是因为底层骨架一致
6. 常见问题
6.1 把 AQS 当成一堆源码细节去背
这样很快就会迷失在节点状态、CAS 细节和模板方法里。
先看骨架,再看细节,学习效率会高很多。
6.2 看不清独占和共享的设计差异
如果这一步没搞懂,后面看 CountDownLatch 和 Semaphore 就会一直觉得混乱。
6.3 不知道同步器为什么能复用统一框架
核心原因就在于:
- 大部分同步器都能抽象成“状态 + 竞争 + 排队 + 唤醒”
7. 动手验证
这一节可以直接复制运行,边看边验证。
7.1 准备一个可运行示例
新建文件 AqsDemo.java,内容如下:
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class AqsDemo {
static class SimpleMutex {
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
boolean isLocked() {
return getState() == 1;
}
}
private final Sync sync = new Sync();
void lock() {
sync.acquire(1);
}
boolean tryLock() {
return sync.tryAcquire(1);
}
void unlock() {
sync.release(1);
}
boolean isLocked() {
return sync.isLocked();
}
}
static class OneShotLatch {
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected int tryAcquireShared(int arg) {
return getState() == 1 ? 1 : -1;
}
@Override
protected boolean tryReleaseShared(int arg) {
setState(1);
return true;
}
}
private final Sync sync = new Sync();
void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
void signal() {
sync.releaseShared(1);
}
}
public static void main(String[] args) throws Exception {
SimpleMutex mutex = new SimpleMutex();
AtomicInteger counter = new AtomicInteger(0);
Thread[] threads = new Thread[4];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
mutex.lock();
try {
counter.incrementAndGet();
} finally {
mutex.unlock();
}
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("exclusiveCounter=" + counter.get());
mutex.lock();
Thread tryLockThread = new Thread(() ->
System.out.println("tryLockWhileHeld=" + mutex.tryLock()));
tryLockThread.start();
tryLockThread.join();
mutex.unlock();
System.out.println("mutexLockedAfterUnlock=" + mutex.isLocked());
OneShotLatch latch = new OneShotLatch();
Thread waiter = new Thread(() -> {
try {
latch.await();
System.out.println("sharedLatchPassed=true");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
waiter.start();
Thread.sleep(100);
latch.signal();
waiter.join();
}
}7.2 编译并运行
javac AqsDemo.java
java AqsDemo如果你在 PowerShell 中操作,也直接执行同样两条命令即可。
7.3 你应该观察到什么
输出不一定逐字完全一致,但应包含这些关键信息:
exclusiveCounter=4000
tryLockWhileHeld=false
mutexLockedAfterUnlock=false
sharedLatchPassed=true7.4 每一行在验证什么
exclusiveCounter=4000:说明基于 AQS 的独占同步器可以正确保护临界区tryLockWhileHeld=false:说明独占状态被占用时,其他线程无法直接获取mutexLockedAfterUnlock=false:说明释放成功后同步状态恢复sharedLatchPassed=true:说明基于 AQS 的共享模式同步器可以实现“等待条件满足再放行”
7.5 再做两个延伸验证
你可以继续做下面两个实验:
- 去掉
SimpleMutex中的mutex.unlock() - 不调用
latch.signal()
你可以观察:
- 独占同步器不释放时,后续线程会一直等待
- 共享模式条件不满足时,等待线程不会通过
8. 练习建议
下面这些练习做完,这一章会更扎实:
- 先从
ReentrantLock反推 AQS 的独占结构 - 阅读一次
CountDownLatch的共享获取和释放流程 - 自己画一张
state + 等待队列 + 唤醒的示意图 - 用“同步状态 + 模板方法”角度重新理解常见同步器
9. 自测问题
- AQS 的核心职责是什么?
state在 AQS 中扮演什么角色?- 独占模式和共享模式的核心差异是什么?
- 为什么很多同步器都能构建在 AQS 之上?
- 等待队列在 AQS 中承担什么职责?
10. 自测核对要点
如果你的回答能覆盖下面这些点,说明这一章基本掌握到位了:
- AQS 是同步器框架,不直接等于某个具体锁
state是同步状态核心- 获取失败线程会进入等待队列
- AQS 同时支持独占和共享两种同步模式
- 很多并发工具之所以可复用,是因为底层都能抽象成“状态 + 队列 + 唤醒”