AQS 是什么?它存在什么问题?
2026年01月15日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
面试官提出这个问题,通常旨在考察:
- 对 Java 并发包底层核心机制的掌握程度:不仅仅是了解
ReentrantLock、CountDownLatch等工具的使用,更是想知道它们共同的实现基石是什么。 - 对框架设计与设计模式的理解:AQS 是“模板方法模式”的经典应用,面试官想看你是否能洞察其设计精髓。
- 深入源码分析和批判性思维的能力:你是否能在理解其强大之处的同事,也看到其历史局限性和设计上的权衡,这体现了你的技术深度和视野。
- 解决实际问题的经验:你是否曾因 AQS 的问题而 “踩坑”,或了解其相关的最佳实践与替代方案。
核心答案
AQS(AbstractQueuedSynchronizer,抽象队列同步器) 是 Java 并发包(java.util.concurrent.locks)中一个构建锁和同步器的核心底层框架。像 ReentrantLock、Semaphore、CountDownLatch 等都是基于它实现的。它通过一个整型的 volatile 状态变量(state) 和一个 FIFO 线程等待队列(CLH 变体) 来管理资源的获取与释放。
AQS 主要存在的问题有:
- API 设计较为复杂且易误用:其
protected方法(如tryAcquire)需要子类精确实现,容易因逻辑错误导致死锁或性能问题。 - 默认的非公平策略可能导致 “线程饥饿”:虽然性能通常更好,但在高竞争下,新来的线程可能一直抢占资源,导致队列中的线程长期等待。
- 功能扩展门槛高:正确、高效地实现一个自定义同步器需要对 AQS 有非常深入的理解,对于普通开发者难度较大。
- 无法很好地支持更丰富的同步模式:例如,
ReadWriteLock基于 AQS 的实现(ReentrantReadWriteLock)在 “读多写少” 场景下,写锁可能因大量读线程而长期饥饿,这是其模型决定的局限性。
深度解析
原理/机制
AQS 的核心思想是,将资源共享状态的管理和线程排队等待的机制解耦。
- 状态管理(
state):这是一个volatile int变量,其含义由子类定义。例如,在ReentrantLock中,state=0表示锁空闲,state>0表示被持有且可重入次数;在Semaphore中,state表示剩余的许可证数量。 - 线程排队(CLH 队列):这是一个双向链表(在 JDK 1.6 后)构成的 FIFO 队列,用于存放获取资源失败的线程。AQS 通过 CAS(Compare-And-Swap) 操作来保证入队、出队的线程安全。
- 模板方法模式:AQS 定义了
acquire()、release()等核心公共方法,这些方法会调用子类必须实现的tryAcquire(int arg)、tryRelease(int arg)等钩子方法。子类只需关注 “如何尝试获取/释放资源” 这一特定逻辑,而 “如何排队、阻塞/唤醒线程” 这些复杂且通用的部分则由 AQS 框架完成。
代码示例
我们来看一个基于 AQS 实现的最简独占锁(不可重入),它清晰地展示了如何与 AQS 协作:
// 一个非常简单的、基于AQS的不可重入互斥锁
public class SimpleMutexLock {
private static final class Sync extends AbstractQueuedSynchronizer {
// 尝试获取锁:当状态为0时,通过CAS将其设为1,表示获取成功
@Override
protected boolean tryAcquire(int acquires) {
assert acquires == 1; // 这里只支持获取1个资源
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 尝试释放锁:将状态从1设回0
@Override
protected boolean tryRelease(int releases) {
assert releases == 1;
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0); // 此处无需CAS,因为只有持有锁的线程才能调用释放
return true;
}
}
private final Sync sync = new Sync();
public void lock() { sync.acquire(1); }
public void unlock() { sync.release(1); }
}
对比分析与最佳实践
- AQS vs.
synchronized:AQS 提供了更灵活、功能更强的同步控制(如可中断、超时、公平性选择、条件变量),但synchronized随着 JVM 优化(锁升级),在低竞争场景下性能已非常好且写法更简洁。 - 公平 vs. 非公平:
ReentrantLock可以选择公平或非公平策略。非公平锁吞吐量高,但可能饿死线程;公平锁保证了顺序,但上下文切换频繁,吞吐量可能下降 20%-30%。在实践中的默认选择通常是非公平锁。 - 最佳实践:
- 优先使用 JUC 包中的高级工具(如
ReentrantLock,Semaphore),而非直接继承 AQS 造轮子。 - 需要自定义复杂同步逻辑时,务必吃透 AQS 的
state管理和队列行为,并编写详尽的单元测试。 - 对于读多写少的场景,可以考虑
StampedLock(乐观读)或ReentrantReadWriteLock(但要注意写锁饥饿问题),并做压测对比。
- 优先使用 JUC 包中的高级工具(如
常见误区
- 误区一:认为 AQS 队列是绝对的公平。实际上,
tryAcquire在入队前会尝试一次直接获取(非公平抢锁),这是其高性能的关键之一。 - 误区二:混淆
state的语义。state是 AQS 框架提供的资源计数工具,其具体含义完全由子类实现决定。 - 误区三:认为基于 AQS 的锁一定比
synchronized快。在低并发场景下,synchronized(偏向锁、轻量级锁)的性能开销可能更小。
总结
AQS 是 JUC 并发大厦的基石,通过 “状态变量 + FIFO 队列” 和 “模板方法模式” 优雅地统一了同步器的实现范式;然而,其复杂的 API、潜在的非公平性以及在某些同步模式下的局限性,是开发者在高级应用时需要清醒认识的问题。