1. AQS框架在Java并发编程中的核心地位
第一次接触Java并发编程时,我盯着ReentrantLock的源码看了整整三天。当终于理解那个神秘的AbstractQueuedSynchronizer(简称AQS)如何运作时,那种顿悟感至今难忘。作为Java并发包(java.util.concurrent.locks)的基石,AQS用不到2000行代码构建了整个Java锁体系的骨架,这种设计哲学值得我们每个Java开发者深入体会。
AQS本质上是一个构建锁和同步器的框架,采用模板方法模式将同步状态的管理、线程排队等通用逻辑封装起来,开发者只需实现tryAcquire、tryRelease等方法即可定制自己的同步组件。这种设计使得从简单的互斥锁到复杂的同步屏障,都可以基于AQS优雅地实现。
提示:AQS内部维护的FIFO队列(CLH队列变种)是其实现高效线程调度的关键,这个队列通过CAS操作实现无锁化线程安全。
2. AQS核心机制深度解析
2.1 状态管理模型
AQS的核心是一个volatile修饰的int型state变量,这个32位的状态字段通过位运算支持多种语义:
- 互斥锁场景:0表示未锁定,1表示锁定
- 读写锁场景:高16位记录读锁数量,低16位记录写锁状态
- Semaphore场景:表示可用许可数
java复制// 典型的状态获取方法
protected final int getState() {
return state;
}
// CAS原子操作更新状态
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
状态变更必须通过CAS保证原子性,这是AQS实现非阻塞算法的关键。我在调试ReentrantLock时发现,当线程首次获取锁时,state会从0变为1;如果是重入,则继续递增。
2.2 CLH队列变种实现
AQS的等待队列采用CLH锁的变体,这种设计有三大优势:
- 通过前驱节点的状态实现高效的自旋阻塞(相比纯粹的忙等待)
- 队列头尾指针采用懒加载模式,减少CAS竞争
- 节点状态(CANCELLED/SIGNAL/CONDITION)实现精确的线程唤醒控制
java复制static final class Node {
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter; // 用于条件队列
}
实际排查死锁问题时,我常用jstack查看线程堆栈中的AbstractQueuedSynchronizer$Node信息,这能清晰展示锁的竞争关系。
3. AQS在JDK中的典型实现
3.1 ReentrantLock的公平与非公平策略
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;
}
公平与非公平锁的实现差异仅在于tryAcquire方法是否检查队列中有等待线程。生产环境中,非公平锁的吞吐量通常更高,但可能引发线程饥饿。
3.2 CountDownLatch的共享模式
与独占模式不同,CountDownLatch采用共享模式实现:
java复制protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
// 自旋CAS直到计数减为0
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
我曾用CountDownLatch实现过分布式系统的本地模拟测试,通过初始化state为节点数量,每个节点完成任务后countDown,主线程await所有节点就绪后再执行验证。
4. AQS高级特性实战
4.1 条件变量(Condition)实现
ConditionObject是AQS的内部类,实现了经典的监视器条件变量模式:
java复制final ConditionObject newCondition() {
return new ConditionObject();
}
每个Condition维护独立的条件队列,await()会将线程从同步队列转移到条件队列,signal()则逆向转移。在实现阻塞队列时,我常用两个Condition分别处理"非空"和"非满"条件。
4.2 自定义同步器示例:二进制开关
下面实现一个简单的开关同步器:
java复制class BinaryLatch {
private static class Sync extends AbstractQueuedSynchronizer {
protected int tryAcquireShared(int ignore) {
return (getState() == 1) ? 1 : -1;
}
protected boolean tryReleaseShared(int ignore) {
setState(1);
return true;
}
}
private final Sync sync = new Sync();
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public void release() {
sync.releaseShared(1);
}
}
这个实现可用于系统初始化场景,release()相当于"开关打开",所有await()的线程会立即通过。
5. AQS性能优化经验
5.1 避免过度同步
在实现高性能计数器时,我发现直接继承AQS可能引入不必要的性能开销。此时可以考虑:
- 对于简单状态管理,优先使用
AtomicInteger - 只在真正需要线程排队时使用AQS
- 考虑使用
StampedLock等更现代的同步器
5.2 调试技巧
- 通过
toString()查看AQS内部状态:
java复制System.out.println(((ReentrantLock)lock).getQueuedThreads());
- 使用JConsole观察线程阻塞情况
- 在死锁场景下,检查节点的waitStatus是否为CANCELLED
5.3 常见陷阱
- 内存可见性问题:自定义tryAcquire方法必须用getState()读取状态,直接访问state变量可能导致可见性问题
- 异常处理不当:AQS不处理InterruptedException,需要调用方正确处理中断
- 状态溢出:重入锁次数超过int最大值会导致计算错误
- 条件变量误用:必须在持有锁时调用await()/signal()
记得有次排查线上问题,发现某个自研同步器的tryRelease实现没有用CAS,导致在高并发下state出现不一致。这个教训让我深刻理解到AQS模板方法中每个CAS操作的重要性。