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 核心组件 作用
statevolatile int 同步状态,不同实现赋予不同含义
CLH 双向队列 存放等待获取锁的线程
独占模式(Exclusive 同一时刻只有一个线程能获取(如 ReentrantLock
共享模式(Shared 多个线程可同时获取(如 SemaphoreCountDownLatch

深度解析

一、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 队列结构。几个关键点:

  • 双向链表:每个 Nodeprevnext 指针,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 搞定。

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

五、AQS 存在什么问题?

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

1. 学习曲线陡峭

AQS 的源码不算长(约 2500 行),但极其精炼。acquire/releaseacquireShared/releaseSharedCondition 队列、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 层面有什么区别?

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

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

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

  3. CountDownLatchCyclicBarrier 有什么区别?

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

常见面试变体

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

记忆口诀

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

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

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

总结

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