1. 为什么AQS是Java面试的必考题?
在Java技术面试中,AbstractQueuedSynchronizer(AQS)几乎是必问的知识点。作为Java并发包的核心基础组件,AQS支撑了Java中绝大多数同步工具的实现。根据我的面试经验,90%以上的Java高级工程师岗位都会涉及AQS相关问题的考察。
AQS的重要性主要体现在三个方面:
- 它是ReentrantLock、CountDownLatch、Semaphore等常用同步工具的基础实现
- 理解AQS能帮助开发者深入掌握Java并发编程的核心思想
- 面试官可以通过AQS问题考察候选人对并发控制的底层理解深度
我在面试候选人时发现,很多开发者虽然能说出AQS的两种同步方式,但对它们的区别和使用场景理解不够深入。这就像知道汽车有油门和刹车,却不清楚什么时候该踩哪个。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. AQS的两种同步方式详解
2.1 独占式同步(Exclusive Mode)
独占式同步是最常见的同步方式,它的核心特点是同一时刻只允许一个线程获取同步状态。这种模式适用于需要排他性访问资源的场景。
以ReentrantLock为例,它的实现就基于AQS的独占模式:
java复制final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
独占式同步的几个关键特点:
- 重入性:已经获取锁的线程可以再次获取(体现在state的累加)
- 公平性:可以通过构造参数选择公平或非公平策略
- 中断响应:支持可中断的获取锁操作
提示:在面试中经常会被问到"ReentrantLock是如何实现可重入的",答案就藏在上述代码中对当前线程的判断和state的处理中。
2.2 共享式同步(Shared Mode)
共享式同步允许多个线程同时获取同步状态,适用于资源可以被多个线程同时访问的场景。典型的实现有CountDownLatch和Semaphore。
以Semaphore的实现为例:
java复制protected int tryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
共享式同步的特点:
- 资源配额:通过state表示可用资源数量
- 并发访问:多个线程可以同时获取资源
- 传播机制:资源释放时会唤醒等待线程
我在实际项目中曾遇到一个典型场景:需要限制某个外部API的调用频率。使用Semaphore的共享模式可以完美解决这个问题,每个线程获取许可后才能调用API,调用完毕释放许可。
3. 两种同步方式的对比与选择
3.1 核心区别对比
| 特性 | 独占式同步 | 共享式同步 |
|---|---|---|
| 线程访问数量 | 单线程独占 | 多线程共享 |
| 典型实现 | ReentrantLock | Semaphore, CountDownLatch |
| 状态表示 | 通常表示持有锁的次数 | 表示可用资源数量 |
| 条件队列 | 支持多个条件队列 | 通常不适用 |
| 适用场景 | 排他性资源访问 | 资源共享场景 |
3.2 如何选择合适的同步方式
选择同步方式时需要考虑以下几个因素:
-
资源特性:资源是否允许多个线程同时访问?如果是,选择共享式;如果必须互斥访问,选择独占式。
-
性能需求:共享式通常具有更好的并发性能,因为允许多线程并行操作。
-
功能需求:是否需要条件队列、可重入等特性?独占式提供更丰富的功能。
我在一个电商项目中遇到过这样的选择:商品库存管理应该使用哪种同步方式?起初使用了独占式同步,导致性能瓶颈;后来改为共享式同步(使用Semaphore表示库存数量),性能提升了3倍。
4. AQS的底层实现原理
4.1 CLH队列与线程阻塞
AQS内部维护了一个CLH(Craig, Landin, and Hagersten)队列,这是一个虚拟的双向队列。当线程获取同步状态失败时,会被封装成Node节点加入队列尾部。
java复制private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
这个队列有几个关键特点:
- 使用CAS操作保证线程安全
- 通过前驱节点的状态决定是否阻塞
- 唤醒时只唤醒头节点的后继节点
4.2 状态管理机制
AQS使用一个volatile int类型的state来表示同步状态。不同的同步器对state的解释不同:
- ReentrantLock:state表示持有锁的线程获取锁的次数
- Semaphore:state表示可用许可的数量
- CountDownLatch:state表示还需要countDown的次数
状态变更通过CAS操作保证原子性:
java复制protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
5. 面试常见问题与回答技巧
5.1 高频面试问题集锦
-
AQS是如何实现可重入的?
- 回答要点:通过state计数和当前线程检查实现
- 示例回答:"当线程尝试获取锁时,AQS会检查当前持有锁的线程是否是请求线程。如果是,则简单增加state值;否则加入等待队列。"
-
共享式和独占式的主要区别是什么?
- 回答要点:访问线程数量、状态表示、适用场景
- 示例回答:"主要区别在于共享式允许多个线程同时获取资源,适用于Semaphore等场景;而独占式同一时刻只允许一个线程访问,适用于锁的实现。"
-
AQS的公平和非公平模式有什么区别?
- 回答要点:新请求线程是否检查队列
- 示例回答:"公平模式下,新请求的线程会先检查队列中是否有等待线程;非公平模式下会直接尝试获取锁,可能插队。"
5.2 回答技巧与注意事项
-
结合源码回答:面试官通常期望听到具体的实现细节,最好能提到关键的方法名和实现逻辑。
-
举例说明:用实际项目经验或常见框架中的使用场景来佐证你的理解。
-
避免死记硬背:理解原理比记住概念更重要,面试官可能会追问细节。
-
承认知识盲区:如果遇到不熟悉的问题,诚实地表示不太清楚,但可以尝试基于已有知识推理。
我在面试候选人时,最欣赏的是那些能够把AQS原理和实际项目经验结合起来的回答。比如有位候选人谈到他们如何通过分析AQS队列长度来诊断系统瓶颈,这种回答往往能获得加分。
6. 实际项目中的应用经验
6.1 性能优化案例
在一个高并发订单处理系统中,我们最初使用ReentrantLock来保护订单状态变更。但在大促期间出现了严重的性能问题。通过分析发现:
- 大多数订单状态变更操作非常快速
- 锁竞争导致大量线程阻塞
- AQS队列长度监控显示平均等待线程数高达50+
解决方案是:
- 将独占锁改为读写锁(ReadWriteLock)
- 对读多写少的操作使用共享锁
- 对写操作使用独占锁但减少临界区范围
优化后系统吞吐量提升了4倍,AQS队列长度降至个位数。
6.2 常见陷阱与规避方法
-
锁泄漏:忘记释放锁会导致系统死锁
- 解决方法:使用try-finally块确保锁释放
-
过度同步:锁范围过大影响性能
- 解决方法:精确控制临界区范围
-
死锁:多个锁获取顺序不一致
- 解决方法:统一锁获取顺序或使用tryLock
-
活锁:线程不断重试但无法进展
- 解决方法:引入随机退避机制
我在项目中曾遇到一个隐蔽的死锁问题:两个线程分别持有A、B锁,然后尝试获取对方持有的锁。通过线程转储分析发现这个问题后,我们制定了团队内的锁获取顺序规范,避免了类似问题。
7. 深入学习AQS的建议
要真正掌握AQS,我建议按照以下路径学习:
-
基础理解:
- 阅读JavaDoc和基本使用示例
- 理解state、CLH队列等核心概念
-
源码分析:
- 从ReentrantLock入手分析独占模式
- 再研究Semaphore的共享模式实现
- 最后分析Condition的实现
-
实践验证:
- 编写简单的同步器实现
- 使用JStack观察AQS队列
- 通过压测验证不同模式性能
-
扩展学习:
- 研究AQS在JUC其他组件中的应用
- 了解AQS的变体和改进(如StampedLock)
我在学习AQS时,发现最有效的方法是边读源码边写测试用例验证。例如,可以继承AQS实现一个简单的二元闭锁,这能帮助理解模板方法模式在AQS中的应用。
