1. 从一把锁看Java并发核心机制
第一次看到AQS(AbstractQueuedSynchronizer)这个名词时,很多Java开发者都会感到困惑——这个藏在java.util.concurrent.locks包里的抽象类,凭什么被称为Java并发包的核心框架?直到我在实际项目中遭遇了各种线程同步问题,才真正理解Doug Lea大师设计这个同步器基类的精妙之处。
想象一下这样的场景:你需要实现一个带超时功能的共享锁,要求支持公平/非公平模式,还要能统计等待线程数。如果从零开始实现,光是处理线程排队、状态维护和唤醒机制就够头疼了。而AQS提供的模板方法模式,让我们只需要实现tryAcquire/tryRelease等几个关键方法,就能快速构建出各种高性能同步组件。ReentrantLock、Semaphore、CountDownLatch这些耳熟能详的工具类,底层都是基于AQS搭建的。
2. AQS架构设计精要
2.1 同步状态的双重含义
AQS的核心是一个volatile修饰的int型state变量,这个看似简单的字段承载着双重职责:
java复制/**
* The synchronization state.
*/
private volatile int state;
在独占模式(如ReentrantLock)下,state表示锁的持有计数:
- 0:锁未被占用
- 1:锁被某线程独占
-
1:锁被重入的次数
而在共享模式(如Semaphore)下,state表示可用许可数量:
- 0:无可用许可
- N:剩余N个许可
这种设计体现了AQS的灵活性——通过不同的实现方式,state可以表达各种同步语义。我在实现自定义同步器时,经常需要仔细规划state的位分配。比如开发读写锁时,可以用高16位表示读锁计数,低16位表示写锁状态。
2.2 CLH队列变体的精妙实现
线程竞争失败后的排队机制是AQS的另一大核心。不同于传统的CLH锁,AQS使用了一个虚拟的双向链表队列:
java复制static final class Node {
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
}
这个设计有几个关键点值得注意:
- 前驱指针(prev):用于实现取消机制,当线程取消等待时,可以快速重建链表
- 后驱指针(next):实际是优化项,非必须存在(可以通过prev遍历)
- nextWaiter:用于区分共享/独占模式
我在排查一个生产环境死锁问题时,曾借助这个队列结构快速定位到某个线程长时间占用锁却未释放的情况。通过dump线程堆栈,可以清晰看到等待队列中的线程状态。
3. 关键模板方法实现解析
3.1 独占模式下的获取与释放
以ReentrantLock的非公平实现为例,其核心是NonfairSync类:
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;
}
这段代码揭示了几个重要细节:
- CAS操作保证原子性
- 重入计数处理
- 溢出保护机制
注意:非公平锁的实现会直接尝试获取锁,不检查队列中是否有等待线程。这在某些场景下可能导致线程饥饿。
3.2 共享模式下的同步控制
CountDownLatch的实现展示了共享模式的典型用法:
java复制protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
// 自旋CAS直到成功
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
共享模式的特点是:
- 允许多线程同时获取
- 释放时需要原子性操作
- 通常需要循环尝试CAS
4. 条件变量的实现机制
AQS中的ConditionObject为同步器提供了更精细的线程控制能力。与内置的wait/notify相比,它的优势在于:
- 多条件队列:单个锁可以创建多个Condition
- 不丢失信号:signal操作发生在lock保护下
- 可中断等待:支持限时和中断响应
java复制public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
这个await实现展示了几个关键步骤:
- 创建条件节点并加入条件队列
- 完全释放锁(处理重入情况)
- 进入等待状态
- 被唤醒后重新竞争锁
5. 性能优化技巧与陷阱规避
5.1 自旋优化的取舍
在AQS的实现中,入队前的短暂自旋是常见的优化手段:
java复制for (int spins = -1;;) {
if (h == t) {
if (++spins >= MAX_HEAD_SPINS)
spins = MAX_HEAD_SPINS;
}
// ...
}
这种优化基于以下假设:
- 短期锁竞争很可能很快结束
- 自旋比挂起/唤醒线程开销小
- 需要合理控制自旋次数
但在实际应用中,我发现这个优化对长耗时同步块效果有限,有时甚至会增加CPU负载。合理的MAX_HEAD_SPINS值需要根据具体场景测试确定。
5.2 取消节点的处理陷阱
节点取消是AQS中最复杂的部分之一。我曾遇到过一个生产问题:取消节点没有正确清理,导致内存泄漏。正确的处理方式应该是:
java复制private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null;
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next;
node.waitStatus = Node.CANCELLED;
if (node == tail && compareAndSetTail(node, pred)) {
pred.compareAndSetNext(predNext, null);
} else {
// ... 其他情况处理
}
}
关键点包括:
- 清理线程引用帮助GC
- 跳过已取消的前驱节点
- CAS保证并发安全
6. 自定义同步器实战
基于AQS实现一个简单的二元闭锁:
java复制public 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);
}
}
这个实现展示了AQS的核心优势:
- 状态管理完全由AQS处理
- 只需关注业务逻辑(信号状态)
- 自动获得阻塞/唤醒机制
在测试这个闭锁时,我发现一个有趣的现象:即使signal()先于await()调用,await()也能立即返回——这是因为AQS会记住信号状态,这正是很多场景需要的语义。
7. AQS的局限与替代方案
虽然AQS功能强大,但在某些场景下并非最佳选择:
- 超大规模并发:当线程数超过万级时,AQS的队列管理可能成为瓶颈
- 特定硬件架构:某些CPU架构(如ARM)的CAS操作性能较差
- 特殊同步需求:如phaser、stamped lock等更高级的同步模式
在这些情况下,可以考虑:
- 使用更轻量级的原子变量
- 基于Thread.yield()的自旋锁
- 特定场景优化的第三方并发库
我曾经在一个高频交易系统中,将基于AQS的锁替换为自旋锁+退让策略,性能提升了约30%。但要注意,这种优化需要严格的性能测试和场景适配。
理解AQS的实现原理,不仅是为了使用它,更是为了在合适的场景做出正确的技术选型。当我们需要实现自定义同步机制时,AQS提供的模板方法模式可以大幅降低开发难度;而当现有工具已经满足需求时,直接使用ReentrantLock等高层API可能是更明智的选择。