AQS 是什么?它存在什么问题?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...,查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 框架设计理解:面试官不仅仅是想知道 AQS 是 "一个队列",更是想知道你是否理解它的 模板方法设计模式——AQS 定义了获取/释放的骨架流程,子类只需实现 "尝试获取" 和 "尝试释放" 的逻辑。

  2. 核心机制掌握:考察你是否清楚 AQS 的三大核心组件——state 状态变量、CLH 双向等待队列、独占/共享两种模式。这三块至少得讲透两块。

  3. 局限性认知:很多候选人只会夸 AQS,但说不出它的局限。能指出 AQS 不支持重入检测、不可中断性受限、不支持超时等待的早期版本等问题,说明你是真正在源码层面理解过。

核心答案

先甩结论:AQS(AbstractQueuedSynchronizer)是 Java 并发包中的一个抽象基类,它用 state 状态变量 + CLH 双向队列实现了一套通用的同步器框架。子类只需实现 "尝试获取/释放" 的逻辑,排队、阻塞、唤醒这些脏活累活 AQS 全包了。

AQS 的核心设计理念——模板方法模式:AQS 负责排队和线程调度,你只管定义 "什么算获取成功、什么算释放完成"。

AQS 核心组件 作用
state(volatile int) 同步状态,不同实现赋予不同含义
CLH 双向队列 存放等待获取锁的线程
独占模式(Exclusive) 同一时刻只有一个线程能获取(如 ReentrantLock)
共享模式(Shared) 多个线程可同时获取(如 Semaphore、CountDownLatch)

深度解析

一、state——AQS 的 "状态机"

state 是 AQS 最核心的字段,用 volatile int 修饰,保证了多线程下的可见性。不同的子类赋予 state 不同的含义:

  • ReentrantLock 中,state = 0 表示无人持有,state = 1 表示被占用,state = 2 表示同一个线程重入了两次
  • ReentrantReadWriteLock 更巧妙,把一个 int 拆成两半用——高 16 位记录读锁数量,低 16 位记录写锁数量
  • Semaphore 中,state 就是还剩多少个许可证
  • CountDownLatch 中,state 就是还需要 countDown() 多少次

对 state 的修改全部通过 CAS 保证原子性,这也是 AQS 高性能的根基。

二、CLH 双向队列——线程排队的 "等候区"

当线程获取锁失败时,AQS 会把它包装成一个 Node 节点,塞进 CLH 双向队列里排队:

上图展示了 AQS 的 CLH 队列结构。几个关键点:

  • 双向链表:每个 Node 有 prev 和 next 指针,head 指向队首(通常是哨兵节点),tail 指向队尾
  • 入队过程:新来的线程 CAS 把自己加到 tail 后面,成为新的 tail
  • 哨兵节点:head 指向的节点是一个空壳(不绑定线程),真正的第一个等待线程是 head.next
  • waitStatus:记录每个节点的状态,最关键的是 SIGNAL——表示 "我后面有人,我释放的时候记得唤醒它"

三、获取锁的完整流程(独占模式)

以 ReentrantLock 为例,一个线程获取锁的完整流程:

上面是独占模式下获取锁的完整链路。整个流程可以概括为三步:

  • 快速尝试:先 tryAcquire(),用 CAS 抢一把 state,抢到就完事了
  • 入队等待:抢不到就把自己包装成 Node,CAS 加入 CLH 队列尾部
  • 自旋 + 挂起:在队列里检查自己是不是队首,是的话再 tryAcquire() 一次;不是的话通过 LockSupport.park() 挂起线程,等待前驱节点释放锁时唤醒

释放锁的流程就简单多了:state 减到 0,找到后继节点(head.next),调用 LockSupport.unpark(succNode.thread) 唤醒它。

四、模板方法——AQS 最优雅的设计

AQS 的源码读起来确实爽,Doug Lea 的设计功力真的服气。核心思想是 模板方法模式:

// AQS 定义的模板方法(骨架流程,子类不能重写)
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&                          // ① 子类实现:尝试获取
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // ② AQS 负责:入队 + 排队 + 挂起
        selfInterrupt();
}

// 子类需要重写的方法(只定义 "什么算获取成功")
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

acquire() 是 final 方法,定义了完整的获取流程。子类只需重写 tryAcquire() 来定义 "什么算获取成功" 和 tryRelease() 来定义 "什么算释放完成"。排队、阻塞、唤醒这些底层机制全部由 AQS 搞定。

这就是为什么 ReentrantLock、Semaphore、CountDownLatch 都基于 AQS,但行为完全不同——它们各自对 state 的含义和 tryAcquire 的判断逻辑有不同的实现。

五、AQS 存在什么问题?

AQS 设计得确实优雅,但它也不是没有局限:

1. 学习曲线陡峭

AQS 的源码不算长(约 2500 行),但极其精炼。acquire/release、acquireShared/releaseShared、Condition 队列、CANCELLED 节点清理……光是理清这些逻辑就要花不少时间。说实话,这块我当年也啃了好几遍才通透。

2. Condition 队列和同步队列的切换复杂

AQS 的 ConditionObject 实现了 Condition 接口,内部维护了一个 单向条件队列。调用 .await() 时,线程从同步队列转移到条件队列;调用 .signal() 时,又从条件队列转移回同步队列。两个队列之间的节点迁移逻辑非常容易搞混,也容易出 bug。

3. 取消节点清理(cancelAcquire)的边界情况

线程在等待过程中可能因为中断或超时被取消。AQS 需要把 CANCELLED 状态的节点从队列中摘除,但这个清理逻辑涉及前驱和后继指针的多次调整,边界条件很多。Doug Lea 本人在注释中也承认这块逻辑 "intricate"(错综复杂)。

4. 不支持非阻塞式获取的回调

AQS 的 tryAcquire() 返回的是 boolean,不支持异步回调。如果你需要 "获取失败时不阻塞,而是回调通知" 这种模式,AQS 原生不支持,需要自己在上层封装。

5. 共享模式的传播机制容易踩坑

共享模式下(如 Semaphore),一个线程释放后可能需要 级联唤醒 多个等待线程(setHeadAndPropagate)。这个 "传播" 逻辑在某些边界条件下可能导致多余唤醒或漏唤醒,Doug Lea 在 JDK 9 中专门修过这个 bug(JDK-8078836)。

面试高频追问

  1. ReentrantLock 的公平锁和非公平锁在 AQS 层面有什么区别?

    FairSync 的 tryAcquire() 多了 hasQueuedPredecessors() 检查——如果队列里有人在等,你就不能插队。NonfairSync 没这个检查,直接 CAS 抢。这就是之前 "公平锁和非公平锁的区别" 那篇里讲的那个 hasQueuedPredecessors()。

  2. AQS 为什么用 CLH 队列而不是普通队列?

    CLH 队列的特点是每个节点只需要 自旋检查前驱节点的状态,而不需要全局同步。这在 NUMA 架构下性能更好,因为线程只需要关注自己前驱节点所在的缓存行,减少了跨 CPU 的缓存一致性开销。

  3. CountDownLatch 和 CyclicBarrier 有什么区别?

    CountDownLatch 基于 AQS(state = 剩余次数),是一次性的,用完不能重置。CyclicBarrier 不基于 AQS,内部用 ReentrantLock + Condition 实现,可以循环使用。

常见面试变体

  • "AQS 的底层原理是什么?"
  • "ReentrantLock 的底层是怎么实现的?"
  • "CLH 队列是什么?AQS 为什么用它?"
  • "AQS 的 state 是怎么用的?"

记忆口诀

AQS 三大件:state(状态)+ CLH 队列(排队)+ 独占/共享(模式)

模板方法:AQS 管排队阻塞唤醒,子类管 "什么算成功"

独占流程:tryAcquire 快抢 → 失败 addWaiter 入队 → acquireQueued 自旋/挂起

总结

AQS 是 Java 并发包的基石,用 state + CLH 双向队列 + 模板方法模式构建了一套通用的同步器框架。ReentrantLock、Semaphore、CountDownLatch 等常用并发工具都基于它实现。核心流程就是:CAS 抢 state → 失败入 CLH 队列 → 自旋或 park 挂起 → 前驱释放时 unpark 唤醒。面试时把 "三大件" 和 "独占获取流程" 讲清楚,再能指出它的局限性,这道题就是高分。