并发06:AQS框架

围绕 ReentrantLock、Semaphore、CountDownLatch 背后的 AbstractQueuedSynchronizer,系统理解 state、CLH 队列、acquire/release 流程和自定义同步器的设计思想。

字数 1352 阅读时长 ≈ 4 分钟 2026-5-26 2026-7-12
并发06:AQS框架

AQS 是什么

AQS,全名 AbstractQueuedSynchronizer,不是业务代码里直接拿来用的锁,而是一套构建同步器的基础框架。ReentrantLockSemaphoreCountDownLatchReentrantReadWriteLock 背后都能看到它的影子。

它要解决的问题很朴素:很多并发工具都需要”抢资源、抢不到就排队、资源释放后再唤醒”。如果每个工具都自己写一套等待队列、线程挂起和唤醒逻辑,成本很高,也容易错。

AQS 把通用部分抽出来:用一个 state 表示同步状态,用一个 FIFO 队列管理等待线程,再把”怎样算获取成功”留给具体同步器实现。

三个核心对象

理解 AQS 可以先抓三件事:

对象作用示例
state同步状态ReentrantLock 中表示重入次数,Semaphore 中表示许可数
等待队列管理等待线程CLH 双向队列,获取失败的线程入队等待
模板方法定义获取/释放流程acquire、release 等方法

AQS acquire 和 release 主线

acquire 获取流程

ReentrantLock 为例,获取锁的流程:

  1. CAS 尝试获取:如果 state == 0,CAS 改成 1,记录 owner,获取成功
  2. 重入判断:如果当前线程已经是 owner,state 加 1
  3. 入队等待:获取失败,线程进入 AQS 队列,通过 LockSupport.park 挂起
boolean tryAcquire(int arg) {
    if (state == 0 && compareAndSetState(0, arg)) {
        owner = currentThread;
        return true;
    }
    if (owner == currentThread) {
        state += arg;
        return true;
    }
    return false;
}

队列节点状态

AQS 队列不是简单链表。节点里会记录等待状态,用来表达:

  • 后继线程是否需要被唤醒
  • 当前节点是否取消
  • 是否在条件队列里等待

线程入队后不会立刻挂起,它会先判断前驱是不是头节点,如果是,就再尝试一次获取。这样能减少不必要的挂起和唤醒成本。

线程被唤醒后也不代表一定拿到锁。唤醒只是给它一次继续竞争的机会。它还要重新执行 tryAcquire,成功才会把自己设置成新的头节点。

公平锁和非公平锁

类型特点适用场景
非公平锁新线程先尝试直接 CAS 抢锁,可能插队高频短临界区,追求高吞吐
公平锁先看队列是否有前驱,有人等就排队核心交易状态更新,追求等待有序

公平不是绝对更好。核心交易状态更新也许更在意等待有序;高频短临界区也许更在意吞吐。项目里选公平锁,要有业务理由,而不是因为”公平”这个词听起来更稳。

Condition 条件队列

Condition 容易被误解成”更灵活的 wait/notify”。它真正做的是把等待条件从同步队列里拆出去:

Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();

线程调用 await() 时,会释放当前锁,进入条件队列;其他线程修改条件后调用 signal(),等待节点才会从条件队列转移回 AQS 同步队列,重新竞争锁。

有界队列、连接池、对象池这类结构,经常需要两个条件:队列非空才能取,队列未满才能放。Condition 的价值就在这里:不同条件可以有不同等待队列,避免所有线程都被一个粗粒度通知唤醒。

共享模式和独占模式

AQS 支持两种模式:

模式特点示例
独占模式同一时刻只有一个线程获取成功ReentrantLock
共享模式多个线程可以同时获取成功Semaphore、CountDownLatch

这也是为什么不同工具看起来行为差很多,但底层模型相似。差异主要在 state 怎么解释,以及获取成功后是否允许继续传播唤醒。

常见误区

误区一:把 AQS 当成普通队列

AQS 队列不是为了存任务,而是为了管理等待同步状态的线程。队列里排的是”谁有资格下一次尝试获取”,不是业务任务。

误区二:以为入队顺序就一定等于公平

AQS 可以支持公平锁,也可以支持非公平锁。非公平锁允许新来的线程先尝试抢一次,抢到了就不进队列。

误区三:只记源码细节,不理解状态含义

学 AQS 最重要的是看具体同步器如何解释 state。同一个字段,在锁、信号量和门闩里表达的是完全不同的业务语义。

项目里为什么要懂 AQS

大多数业务系统不需要自己继承 AQS 写同步器,但理解它能帮你判断并发工具的边界:

  • CountDownLatch 只能倒数一次,归零后不能重置;需要重复使用看 CyclicBarrierPhaser
  • Semaphore 控制的是许可数,不等于线程池,适合限制同时访问某个资源的并发量
  • ReentrantLock 的公平锁能减少饥饿,但可能牺牲吞吐

线上排查时,如果线程栈里大量出现 LockSupport.park,就知道线程在等待同步条件。下一步要找的是谁持有锁、谁没有释放许可、哪个条件没有被 signal。

理解方式

看到一个 AQS 工具,可以问三句话:

  1. state 表示什么
  2. 获取失败的线程什么时候入队,什么时候被唤醒
  3. 释放动作会唤醒一个线程,还是传播给多个线程

这三句话比背完整源码更有用。AQS 的价值不是让我们炫技写锁,而是让我们看懂 Java 并发工具背后共同的排队和唤醒模型。