1. 为什么AQS是多线程面试的必考题
第一次被面试官问到AQS时,我整个人都是懵的。那是我大三参加某大厂实习面试,面试官让我解释ReentrantLock的实现原理。当我支支吾吾说到"好像用了什么队列"时,面试官直接打断我:"你连AQS都不知道?" 那次惨痛的教训让我明白,AQS(AbstractQueuedSynchronizer)确实是Java并发编程的基石,更是面试中的高频考点。
AQS的重要性体现在三个方面:首先,它是Java并发包的核心框架,像ReentrantLock、CountDownLatch、Semaphore这些常用工具类都是基于AQS实现的;其次,它采用了经典的模板方法模式,是设计模式应用的典范;最后,理解AQS能帮助我们深入掌握线程阻塞、唤醒等底层机制。我在美团工作时,团队里有个不成文的规定——不会AQS的候选人直接pass,因为这意味着他对Java并发理解不够深入。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. AQS的核心设计思想
2.1 同步状态的管理艺术
AQS最精妙的设计在于用单个int变量(state)表示同步状态。这个state的具体含义由子类定义,比如:
- ReentrantLock中表示重入次数
- Semaphore中表示剩余许可数
- CountDownLatch中表示剩余计数
通过CAS操作修改state值,AQS实现了无锁化的状态变更。我在阅读源码时发现,Doug Lea(AQS作者)特意将state声明为volatile,保证可见性,同时所有状态修改都通过Unsafe类提供的CAS操作完成。这种设计既保证了线程安全,又避免了重量级锁的开销。
java复制// AQS中state的相关定义
private volatile int state;
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
2.2 CLH队列的变体实现
当线程获取锁失败时,AQS会将线程封装成Node节点加入CLH队列。这个队列有几点值得注意:
- 它是CLH锁的变体,但做了优化:原始的CLH是自旋锁,而AQS中线程会park阻塞
- 队列使用双向链表实现,方便处理取消的节点
- 每个节点会保存waitStatus,表示线程状态(CANCELLED、SIGNAL等)
我在调试ReentrantLock时发现,队列的头节点是个"虚节点",它不关联任何线程,只是为了简化边界条件处理。真正的第一个等待线程是头节点的下一个节点。
2.3 模板方法模式的经典应用
AQS采用了模板方法模式,定义了两种操作方式:
- 独占模式(如ReentrantLock)
- 共享模式(如Semaphore)
子类只需要实现tryAcquire/tryRelease或tryAcquireShared/tryReleaseShared等方法,AQS会负责线程排队、阻塞唤醒等复杂逻辑。这种设计使得:
- 同步器实现者只需关注状态管理
- 避免了重复实现队列管理代码
- 保证了线程调度的正确性
3. 从ReentrantLock看AQS实战
3.1 公平锁与非公平锁的实现差异
ReentrantLock的公平性是通过hasQueuedPredecessors()方法实现的。非公平锁会直接尝试获取锁,而公平锁会先检查队列中是否有等待线程:
java复制// 非公平锁的获取方式
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// 公平锁的检查逻辑
protected final boolean tryAcquire(int acquires) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
return false;
}
我在压测中发现,非公平锁的吞吐量比公平锁高5-10倍,但可能导致线程饥饿。实际开发中,除非有严格顺序要求,否则建议使用非公平锁。
3.2 条件变量的实现原理
ConditionObject是AQS的内部类,实现了条件变量功能。每个ConditionObject都有自己的等待队列,与同步队列独立。await()方法会将线程从同步队列转移到条件队列,signal()方法则相反。
这里有个容易踩的坑:调用await()前必须持有锁,否则会抛IllegalMonitorStateException。我在线上就遇到过这个问题,当时排查了半天才发现是漏了加锁。
4. AQS的常见面试题深度解析
4.1 为什么AQS使用双向链表
面试时被问到这个问题,我最初回答是为了方便遍历。后来读源码才发现主要原因是:
- 取消节点时需要快速找到前驱节点(node.prev)
- 在条件等待时,需要将节点从条件队列转移到同步队列
- 处理取消节点时,需要修改后继节点的指针
单向链表无法高效完成这些操作。Doug Lea在注释中特别说明,虽然双向链表占用更多内存,但能简化算法实现。
4.2 AQS为什么没有用Java的并发集合
这个问题考察对AQS定位的理解。AQS是并发包的基础框架:
- 它需要更底层的控制,比如精确的内存可见性保证
- 它要避免依赖其他并发工具,防止循环依赖
- 它需要处理线程中断、超时等复杂场景
实际上,AQS直接使用Unsafe类操作内存,比Java并发集合更接近底层。
5. AQS的实战注意事项
5.1 正确实现tryAcquire方法
实现自定义同步器时,tryAcquire的正确写法应该:
- 先读取当前state
- 检查是否符合获取条件
- 使用CAS修改state
常见错误是忘记用CAS,直接修改state。我在第一次实现自定义锁时就犯了这个错,导致出现竞态条件。
5.2 处理中断的正确方式
AQS的acquireInterruptibly()方法会响应中断,但acquire()不会。这导致一个常见陷阱:用acquire()获取锁的线程被中断后,会继续等待锁,只是中断标志位被设置。
正确的做法是根据业务需求选择适当的方法,如果需要响应中断,就使用acquireInterruptibly()。
6. 从AQS看Java并发设计哲学
AQS体现了Java并发设计的几个核心理念:
- 分离不变与可变:AQS处理不变的队列管理,子类处理可变的状态逻辑
- 乐观并发控制:优先尝试CAS操作,失败才排队
- 减少上下文切换:通过park/unpark精准控制线程阻塞唤醒
理解这些思想比死记硬背AQS源码更重要。我在面试候选人时,更看重他能否从AQS延伸到这些设计原则的讨论。
