1. AQS核心机制解析
AbstractQueuedSynchronizer(AQS)作为Java并发包的基石,其设计精髓在于通过CLH队列(Craig, Landin, and Hagersten lock queue)的变体实现线程阻塞与唤醒机制。这个双向链表结构的等待队列,每个节点保存着线程引用和等待状态,通过CAS原子操作保证线程安全。
关键细节:CLH队列的prev指针主要服务于取消机制,而next指针实现真正的唤醒链。当线程获取锁失败时,会通过LockSupport.park()进入WAITING状态。
AQS内部维护的state变量(volatile int)是整个同步器的核心状态标识。以ReentrantLock为例,state=0表示锁未被占用,state=1表示被占用,state>1则是重入计数。不同的同步器对state的语义解释各不相同:
- Semaphore:state表示可用许可数
- CountDownLatch:state表示剩余计数
- ReentrantReadWriteLock:高16位记录读锁,低16位记录写锁
java复制// 典型tryAcquire实现模板
protected boolean tryAcquire(int arg) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, arg)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
setState(c + arg); // 重入处理
return true;
}
return false;
}
2. 独占与共享模式实现差异
2.1 独占式同步
典型代表ReentrantLock的实现包含三个关键方法:
- tryAcquire:尝试获取资源(需子类实现)
- acquireQueued:加入CLH队列并自旋检查
- shouldParkAfterFailedAcquire:检查前驱节点状态决定是否阻塞
线程中断处理流程特别值得注意:
- 在acquireInterruptibly()中立即响应中断
- 常规acquire()只在获取成功后补发中断
2.2 共享式同步
CountDownLatch和Semaphore采用共享模式,核心区别在于:
- tryAcquireShared返回剩余资源量(负值表示失败)
- 成功获取后会通过setHeadAndPropagate传播唤醒信号
java复制// Semaphore的release实现
public void release() {
sync.releaseShared(1);
}
// AQS中的模板方法
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared(); // 唤醒后继节点
return true;
}
return false;
}
3. 条件队列实现原理
ConditionObject是AQS的内部类,构成独立的条件等待队列。与CLH队列不同,条件队列是单向链表,通过firstWaiter/lastWaiter维护。await()操作的关键步骤:
- 创建CONDITION状态节点加入条件队列
- 完全释放当前线程持有的所有锁(考虑重入)
- 阻塞直到被signal或中断
- 重新竞争锁
signal()操作将节点从条件队列转移到CLH队列,但不会立即唤醒线程,而是在持有锁的线程执行release后才真正唤醒。这种设计避免了"信号丢失"问题。
实战经验:避免在循环外交代条件判断,必须使用while(condition)而非if(condition),防止虚假唤醒。
4. 模板方法设计模式应用
AQS采用典型的模板方法模式,需要子类实现的5个关键方法:
| 方法名 | 独占模式 | 共享模式 | 必须实现 |
|---|---|---|---|
| tryAcquire | ✓ | ✗ | 选其一 |
| tryRelease | ✓ | ✗ | 选其一 |
| tryAcquireShared | ✗ | ✓ | 选其一 |
| tryReleaseShared | ✗ | ✓ | 选其一 |
| isHeldExclusively | 条件需要 | 条件需要 | 可选 |
实现自定义同步器时,通常只需要覆盖部分方法。例如实现一个简单的二元闭锁:
java复制class BooleanLatch {
private static class Sync extends AbstractQueuedSynchronizer {
boolean isSignalled() { return getState() != 0; }
protected int tryAcquireShared(int ignore) {
return isSignalled() ? 1 : -1;
}
protected boolean tryReleaseShared(int ignore) {
setState(1);
return true;
}
}
private final Sync sync = new Sync();
public boolean isSignalled() { return sync.isSignalled(); }
public void signal() { sync.releaseShared(1); }
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
}
5. 性能优化关键点
5.1 自旋优化
在入队后的第一次尝试获取时,如果前驱是头节点,会进行自旋尝试(spinForTimeoutThreshold=1000ns)。这个设计基于"短暂持有"的统计假设,能显著减少线程切换开销。
5.2 取消机制
取消节点的处理包含三个关键步骤:
- 将node.waitStatus置为CANCELLED
- 移除前驱节点中指向该节点的next指针
- 将node.next指向自己(帮助GC)
5.3 头节点优化
成功获取资源的线程会将原头节点出队,新头节点的thread和prev属性置为null。这种"忘记"策略减少GC Roots引用,同时保证后续线程能正确访问prev指针。
6. 常见问题排查指南
问题1:线程卡在acquireQueued
- 检查tryAcquire实现是否正确处理重入
- 确认state的CAS操作没有ABA问题
- 使用jstack查看线程栈,确认前驱节点状态
问题2:死锁发生在Condition.await
- 确认signal/signalAll被正确调用
- 检查await前后锁的获取释放是否配对
- 排查是否有未处理的InterruptedException
问题3:共享模式资源泄漏
- 确保tryReleaseShared返回true时才调用doReleaseShared
- 检查state的增减是否平衡
- 使用Semaphore时注意acquire/release调用次数匹配
问题4:性能瓶颈
- 考虑用ReentrantReadWriteLock替代纯独占锁
- 评估StampedLock的乐观读是否适用
- 对于短时操作,尝试自旋锁替代
7. 面试深度问题剖析
Q1:为什么AQS选择CLH队列而非MCS队列?
CLH队列只需原子更新tail指针,释放时只需检查前驱状态,更适合SMP架构。MCS则需要维护每个节点的locked状态,在NUMA架构下表现更好但实现更复杂。
Q2:如何实现可重入的共享锁?
需要同时记录持有线程和重入次数。可以参考ReadWriteLock的实现:
- 用高16位记录读锁数量
- 低16位记录写锁信息
- 使用ThreadLocal保存各线程的重入计数
Q3:设计一个支持超时的转账锁
关键点在于:
- 继承AQS实现tryAcquire检查账户ID
- 使用tryAcquireNanos支持超时
- 维护全局的账户锁映射表
- 注意处理死锁检测和中断
java复制class AccountLock extends AbstractQueuedSynchronizer {
private static final Map<String, AccountLock> locks = new ConcurrentHashMap<>();
public static boolean transfer(String from, String to, long timeout) throws InterruptedException {
AccountLock lock1 = locks.computeIfAbsent(from, k -> new AccountLock());
AccountLock lock2 = locks.computeIfAbsent(to, k -> new AccountLock());
// 按固定顺序获取锁避免死锁
AccountLock first = from.compareTo(to) < 0 ? lock1 : lock2;
AccountLock second = first == lock1 ? lock2 : lock1;
long nanos = TimeUnit.MILLISECONDS.toNanos(timeout);
if (!first.tryAcquireNanos(1, nanos) ||
!second.tryAcquireNanos(1, nanos - (System.nanoTime() - nanos))) {
first.release();
return false;
}
try {
// 执行转账操作
return true;
} finally {
second.release();
first.release();
}
}
protected boolean tryAcquire(int acquires) {
return compareAndSetState(0, 1);
}
protected boolean tryRelease(int releases) {
setState(0);
return true;
}
}
8. 源码阅读路线建议
- 从ReentrantLock入手理解独占模式
- 分析Semaphore掌握共享模式
- 研究CountDownLatch的闭锁机制
- 深入ReadWriteLock的位运算技巧
- 最后研读ConditionObject的条件队列
重点关注的JDK类:
- AbstractQueuedSynchronizer
- AbstractOwnableSynchronizer
- LockSupport
- AtomicReferenceFieldUpdater
调试技巧:
- 使用-XX:+ShowHiddenFrames显示JVM隐藏帧
- 通过jconsole观察线程阻塞状态
- 使用BTrace监控state变量变化
