1. 从黑盒到白盒:为什么每个Java开发者都应该掌握AQS
作为Java并发编程的核心基础设施,AbstractQueuedSynchronizer(AQS)就像操作系统中的内核调度器,默默支撑着各种同步工具的高效运转。我仍然记得第一次阅读ReentrantLock源码时的震撼——原来那些看似简单的lock()和unlock()背后,隐藏着如此精妙的设计。
理解AQS的价值不仅在于应付面试,更重要的是它能让你:
- 真正看懂JUC包中各种同步工具的工作原理
- 在遇到并发问题时能快速定位到根源
- 设计出更高效的定制化同步组件
- 培养出对并发控制的直觉判断力
2. AQS核心架构解析
2.1 状态机与队列的完美结合
AQS的精妙之处在于它用最简单的两个组件构建出了最灵活的同步框架:
java复制// 同步状态 - 不同工具赋予不同语义
private volatile int state;
// 等待队列 - CLH锁的变体实现
private transient volatile Node head;
private transient volatile Node tail;
这个设计就像交通信号系统:
state相当于红绿灯状态(0表示红灯/资源不可用,>0表示绿灯/资源可用)- 队列就是等待通行的车辆队伍
- 每个Node节点就是一辆车的"驾驶证"(包含线程信息和等待状态)
2.2 状态变量的多态性
AQS的state字段就像变色龙,在不同场景下呈现不同含义:
| 同步工具 | state语义 | 特殊处理 |
|---|---|---|
| ReentrantLock | 锁的重入次数(0=未锁定,1=锁定,>1=重入) | 记录当前持有锁的线程 |
| Semaphore | 可用许可数量 | 允许多个线程同时获取 |
| CountDownLatch | 倒计时计数器 | 达到0时释放所有等待线程 |
| ReadWriteLock | 高16位=读锁计数,低16位=写锁计数 | 位运算分离读写状态 |
这种设计体现了"组合优于继承"的原则——通过单一state字段的不同解释,避免了为每种同步工具创建独立基类。
3. 深入AQS实现机制
3.1 同步队列工作原理
当线程获取资源失败时,AQS的入队流程堪称并发编程的典范:
- 创建节点:将当前线程包装为Node,模式分为EXCLUSIVE(独占)和SHARED(共享)
- 快速入队:通过CAS操作将节点添加到队列尾部
- 挂起线程:调用LockSupport.park()进入WAITING状态
java复制// 简化版的入队逻辑
final boolean acquireQueued(Node node, int arg) {
boolean interrupted = false;
try {
for (;;) {
Node p = node.predecessor();
if (p == head && tryAcquire(arg)) { // 只有前驱是头节点才尝试获取
setHead(node);
p.next = null;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
关键点:AQS采用CLH变体队列而非严格CLH,主要优化了取消操作和超时处理
3.2 条件队列的精准调度
ConditionObject的实现展示了AQS最精妙的部分——它用完全Java代码实现了管程的等待/通知机制:
java复制public class ConditionObject implements Condition {
private transient Node firstWaiter; // 条件队列头
private transient Node lastWaiter; // 条件队列尾
public final void await() throws InterruptedException {
Node node = addConditionWaiter(); // 加入条件队列
int savedState = fullyRelease(node); // 完全释放锁
while (!isOnSyncQueue(node)) {
LockSupport.park(this); // 挂起线程
}
// 被唤醒后重新竞争锁
}
public final void signal() {
Node first = firstWaiter;
if (first != null)
doSignal(first); // 转移节点到同步队列
}
}
与synchronized的wait/notify相比,Condition的优势在于:
- 支持多个等待条件(如ArrayBlockingQueue的notFull和notEmpty)
- 提供可中断和超时的等待
- 实现公平或非公平的唤醒策略
4. 实战:手写可重入锁
4.1 基础实现方案
让我们基于AQS实现一个完整的可重入锁:
java复制public class ReentrantAqsLock implements Lock {
private final Sync sync = new Sync();
private static class Sync extends AbstractQueuedSynchronizer {
// 获取锁
protected boolean tryAcquire(int acquires) {
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; // 重入计数
setState(nextc);
return true;
}
return false;
}
// 释放锁
protected boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
}
// 实现Lock接口方法...
public void lock() { sync.acquire(1); }
public void unlock() { sync.release(1); }
}
4.2 性能优化技巧
在实际实现中,还需要考虑以下优化点:
- 自旋尝试:在入队前先自旋几次尝试获取锁
- 队列优化:减少不必要的CAS操作
- 取消处理:完善线程中断和超时的节点清理
- 内存屏障:合理安排volatile变量的读写顺序
java复制// 优化后的获取逻辑
final boolean nonfairTryAcquire(int acquires) {
Thread current = Thread.currentThread();
for (;;) {
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;
}
if (getExclusiveOwnerThread() != current)
return false;
}
}
5. AQS的最佳实践与陷阱规避
5.1 正确使用模式
- 模板方法实现:只重写tryAcquire/tryRelease或tryAcquireShared/tryReleaseShared
- 状态管理:对state的操作必须保证原子性和可见性
- 条件等待:使用ConditionObject而非Object.wait/notify
- 资源释放:确保在finally块中释放锁
5.2 常见问题排查
死锁场景:
java复制// 错误示例:不可重入锁的重入调用
NonReentrantLock lock = new NonReentrantLock();
lock.lock();
lock.lock(); // 这里会导致线程永久阻塞
解决方案:
- 使用可重入锁实现
- 检查锁的获取和释放是否配对
- 避免跨方法锁传递
性能瓶颈:
- 过多线程竞争同一个锁时,AQS的CAS操作可能成为瓶颈
- 解决方案:考虑减小锁粒度或使用读写锁
6. AQS的衍生应用
6.1 自定义同步工具示例:限流器
java复制public class SimpleRateLimiter {
private final Sync sync;
private final long interval;
public SimpleRateLimiter(int permitsPerSecond) {
this.interval = 1000_000_000L / permitsPerSecond;
this.sync = new Sync();
}
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected int tryAcquireShared(int acquires) {
for (;;) {
long now = System.nanoTime();
long last = getState();
long next = last + interval;
if (now >= next) {
if (compareAndSetState(last, now))
return 1;
} else {
return -1;
}
}
}
}
public void acquire() {
sync.acquireShared(1);
}
}
6.2 与JVM同步机制对比
| 特性 | AQS实现 | synchronized |
|---|---|---|
| 实现方式 | Java代码 | JVM内置 |
| 等待队列 | 显式CLH队列 | 隐式队列 |
| 公平性 | 可配置 | 非公平 |
| 条件等待 | 多条件支持 | 单条件 |
| 锁中断 | 支持 | 不支持 |
| 超时控制 | 支持 | 不支持 |
| 性能 | 更高 | 优化后接近 |
在实际项目中,我通常会根据以下原则选择同步方案:
- 简单同步需求用synchronized
- 需要高级功能(如条件队列、可中断)用AQS系工具
- 极高并发场景考虑使用StampedLock或并发容器
理解AQS的设计哲学,让我在解决各种并发问题时能够游刃有余。它教会我们:优秀的框架设计应该像AQS一样,把不变的部分固化下来,把变化的部分开放出去。这种"框架+策略"的模式,值得我们在设计其他系统时借鉴。