一文彻底搞懂 AQS 的实现原理与设计思想

在面试中,AQS(AbstractQueuedSynchronizer)是一个常见的高频问题。面试官一问“你了解 AQS 吗”,你脑海里只蹦出几个词:“CAS”、“state”、“等待队列”,再深一点就答不上来了。如果让你亲自设计一个并发工具类,你可能连思路都没有。

别急,今天我们就从“如果你要自己实现一个并发工具类”这个角度出发,一步步拆解 AQS 的设计逻辑,并理解为什么它要这么做,为什么这么设计。


什么是并发工具类?实现一个并发工具类要解决什么?

所谓并发工具类,就是多个线程能安全访问共享资源的工具。

你得考虑几个核心问题:

  1. 如何判断共享资源是否正在被其他线程访问?
  2. 如果资源已被占用,新来的线程该怎么办?等?让?走?
  3. 如何管理多个等待线程?
  4. 如何在资源释放后有序地唤醒它们?

一、用 state 表示资源占用状态

最基础的思路就是:用一个变量 state 表示资源状态。

  • state = 0:资源空闲
  • state = 1:资源已被某个线程占用

那你可能会问:为啥不用 boolean 呢?

那是因为有些资源支持多个线程同时访问(比如信号量 Semaphor),也有些场景支持同一线程重复加锁(比如 ReentrantLock 可重入锁)。这时候 state 就不能只表示两种状态了,必须用 int 类型。


二、保证可见性与原子性:volatile + CAS

既然是多线程访问,那修改 state 时必须是线程安全的

  • 可见性:加上 volatile,保证一个线程改了,其他线程立刻能看见。
  • 原子性:使用 CAS(Compare-And-Swap) 操作来原子修改 state

线程尝试通过 CAS 修改 state,如果成功,就表示成功获得了锁或资源;如果失败了,说明资源已被其他线程占用。


三、CAS 失败怎么办?阻塞 + 等待队列

你不能无限 CAS 啊,一直自旋会浪费 CPU。

怎么办?失败就阻塞线程,并将它加入一个等待队列,等资源释放后唤醒它重新尝试。

为了管理这些线程,AQS 设计了一个基于 双向链表 的等待队列:

  • 每个等待线程被封装为一个 Node 节点
  • 入队时挂到尾部,唤醒时从头部依次唤醒

先进先出(FIFO)队列的设计正好可以实现公平策略


四、再抽象一层:开放 tryAcquire() 给子类重写

AQS 并不直接定义怎么“获取资源”,它将这个动作留给子类去实现。

1
2
3
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}

这就是模板方法设计模式的体现,AQS 只关心流程控制,不关心获取资源的细节。

你实现自己的并发工具类时,只需要重写 tryAcquire,即可定义加锁逻辑。


五、acquire vs tryAcquire:阻塞式与非阻塞式

AQS 定义了两种资源获取方式:

  • tryAcquire()尝试获取,失败就返回 false,不阻塞
  • acquire()必须获取,失败就进队阻塞,直到成功

这是为上层业务场景考虑:

  • 有些业务能容忍失败(例如尝试锁)
  • 有些业务必须获取锁才能继续

六、公平锁与非公平锁

AQS 通过队列的 FIFO 特性,提供了公平锁的基础能力。

  • 公平锁:先来的线程先获取资源(先进先出)
  • 非公平锁:允许后来线程插队,跳过队列直接抢锁

例如:

1
2
ReentrantLock fairLock = new ReentrantLock(true);
ReentrantLock unfairLock = new ReentrantLock(false);

这背后的实现,就是重写了 AQS 的 tryAcquire() 方法,是否检查前驱节点是否存在。


七、共享模式与独占模式

AQS 支持两种模式:

模式 说明 应用示例
独占 同一时间只能一个线程访问资源 ReentrantLock
共享 允许多个线程同时访问资源 Semaphore、CountDownLatch

独占模式下:

  • state 表示重入次数
  • 加锁时 CAS 将 state 从 0 改为 1
  • 解锁时减 state,减到 0 就完全释放

共享模式下:

  • state 表示许可数量(如 Semaphore 初始为 3)
  • 获取时减 state,放行多个线程
  • 释放时加 state

八、Condition 的实现:条件队列

使用 ReentrantLock 时经常会看到 Condition

1
Condition condition = lock.newCondition();

这是 AQS 的另一部分:条件队列

  • 没拿到锁的线程:放入 AQS 的 等待队列
  • 拿到锁但资源未就绪、主动放弃执行的线程:放入 Condition条件队列

调用 await() 就会进入条件队列,等待 signal() 被唤醒,再进入等待队列重新竞争锁。

这比 synchronized 更灵活:你可以定义多个 Condition,对应多个等待条件和通知策略。


九、为什么不用操作系统的 Mutex?

AQS 不直接用操作系统的 Mutex 锁,而是自己设计了状态变量、CAS、自定义队列,目的就是性能更高、灵活性更强

原因:

  • Mutex 涉及系统调用(用户态 -> 内核态),开销大
  • AQS 所有逻辑都在用户态完成,只有极端情况下才阻塞
  • 可以扩展公平策略、共享模式、重入等机制
  • 支持各种并发工具的构建,如锁、信号量、栅栏、计数器等

总结一下 AQS 的精髓

AQS 是一个抽象的同步器框架,本质上是:

  • 一个 volatile int state 变量,表示共享资源状态
  • 一个 FIFO 等待队列,管理被阻塞的线程
  • 一个模板方法结构,子类只需要重写资源获取/释放逻辑

其核心能力包括:

✅ 支持独占与共享模式
✅ 支持公平与非公平策略
✅ 支持可重入、可中断、限时等待
✅ 支持条件队列(Condition)
✅ 高性能:用户态自旋 + 阻塞等待的混合机制