1. 为什么需要深入理解AQS?
在Java并发编程的世界里,AQS(AbstractQueuedSynchronizer)就像是一座隐藏在水面下的冰山。你可能每天都在使用基于AQS构建的工具(如ReentrantLock、CountDownLatch),但真正理解其工作原理的人却不多。我第一次阅读AQS源码时,那种"既熟悉又陌生"的感觉至今难忘——熟悉是因为接口简单,陌生是因为内部实现精妙得令人惊叹。
AQS的核心价值在于它提供了一个框架,用于构建依赖先进先出(FIFO)等待队列的锁和同步器。CLH队列(Craig, Landin, and Hagersten lock queue)的变体实现是其精髓所在。与直接使用synchronized关键字相比,AQS系同步器在性能、灵活性和功能扩展性上都有显著优势。这也是为什么Java并发包中诸如Semaphore、ReentrantReadWriteLock等重量级组件都选择AQS作为基础。
提示:阅读AQS源码前,建议先掌握volatile变量、CAS操作和基本的链表数据结构,这些是理解AQS实现细节的必备知识。
2. AQS的核心数据结构剖析
2.1 同步状态与节点结构
AQS的核心是一个volatile修饰的int型状态变量(state)和一个CLH队列。状态变量的含义由子类定义,比如在ReentrantLock中表示持有锁的计数,在Semaphore中则表示可用许可数。这个设计体现了模板方法模式的精妙——AQS处理排队和阻塞的通用逻辑,而子类定义具体的同步语义。
节点(Node)是构成CLH队列的基本单元,每个等待线程都会被封装成一个Node对象。Node类中有几个关键字段值得注意:
java复制static final class Node {
volatile int waitStatus; // 等待状态:CANCELLED、SIGNAL等
volatile Node prev; // 前驱节点
volatile Node next; // 后驱节点
volatile Thread thread; // 关联的线程
Node nextWaiter; // 用于条件队列的特殊链接
}
2.2 CLH队列的变体实现
原始的CLH队列是自旋锁的一种形式,而AQS对其做了重要改进:
- 将自旋改为阻塞:通过LockSupport.park/unpark实现线程挂起和唤醒
- 增加了超时和取消机制:通过waitStatus标记节点状态
- 支持独占和共享两种模式:通过nextWaiter字段区分
队列的头部(head)总是指向一个虚节点(dummy node),这个设计简化了边界条件处理。当线程尝试获取锁失败时,会经历以下步骤:
- 创建节点并入队
- 自旋检查前驱节点是否为head(即是否轮到自己)
- 如果前驱是head则再次尝试获取锁
- 获取失败则安全挂起(通过LockSupport)
3. 独占模式下的锁获取与释放
3.1 acquire方法的完整流程
独占模式是理解AQS的最佳切入点。以ReentrantLock为例,lock()操作最终会调用AQS的acquire方法:
java复制public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这个方法体现了AQS的经典"try-before-queue"策略:
- tryAcquire:由子类实现的具体获取逻辑(如检查state是否为0)
- addWaiter:创建独占模式节点并入队
- acquireQueued:在队列中自旋/阻塞等待
- selfInterrupt:恢复中断状态
3.2 入队操作的细节魔鬼
addWaiter方法中有几个精妙的实现细节:
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;
}
这里采用了"乐观入队"策略:先尝试快速插入队尾,如果失败(说明有竞争)则进入完整的enq方法。enq方法通过CAS+自旋保证最终成功入队,这种分层处理的思想在并发编程中很常见。
注意:在并发环境下设置节点前后指针的顺序至关重要。必须先通过CAS设置tail指针,再建立前驱关系,最后才建立后继关系。这个顺序保证了即使出现竞争,队列也不会出现断裂。
4. 条件队列的实现机制
4.1 条件队列与同步队列的关系
AQS的条件等待功能通过ConditionObject实现,它维护了一个独立的条件队列。与主同步队列不同,条件队列是单向链表,使用nextWaiter字段链接节点。当调用await()时:
- 当前线程创建节点加入条件队列
- 完全释放持有的锁(包括重入次数)
- 阻塞直到被signal或中断
当调用signal()时:
- 将条件队列的头节点转移到主同步队列
- 该节点将在主队列中重新竞争锁
4.2 await/signal的典型问题
在实际使用中,条件队列有几个常见陷阱:
- 虚假唤醒:即使没有signal也可能从await返回,因此条件判断必须用while循环
java复制while (!condition) {
condition.await();
}
-
signal丢失:如果在await之前调用signal,通知会丢失。这与Object.wait/notify的行为一致。
-
跨锁signal:signal必须在持有与ConditionObject关联的锁时调用,否则会抛出IllegalMonitorStateException。
5. 共享模式与传播机制
5.1 共享锁的获取与释放
共享模式(如Semaphore)与独占模式的主要区别在于:
- 获取成功时会传播唤醒后续共享节点
- 释放时需要计算可用资源并决定唤醒多少后继节点
核心方法doAcquireShared的流程:
java复制private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
5.2 传播机制的实现细节
setHeadAndPropagate方法体现了共享模式的精髓:
- 设置新head(与独占模式相同)
- 如果还有剩余资源(propagate > 0),则继续唤醒后继共享节点
- 即使没有剩余资源,如果后继节点正在等待共享访问,也会尝试唤醒
这种传播机制使得资源可以快速分配给多个等待线程,提高了吞吐量。在Semaphore中,这表现为多个线程可以同时获取许可;在CountDownLatch中,表现为所有等待线程可以同时被释放。
6. 取消与超时处理
6.1 节点取消的完整流程
当线程在队列中等待时,可能会因为超时或中断而取消等待。cancelAcquire方法处理这种情况:
- 清空节点关联的线程
- 将waitStatus设置为CANCELLED
- 跳过已取消的前驱节点
- 如果节点是tail,则尝试CAS更新tail
- 如果前驱是head或者已signal,则唤醒后继节点
这个过程中最复杂的是处理取消节点的出队操作,因为可能有其他线程正在并发修改队列。AQS采用"标记-清除"的策略:先标记节点为取消状态,再由后续线程逐步清理。
6.2 超时控制的实现
带超时的acquire方法(如tryAcquireNanos)需要处理时间计算和剩余时间判断:
java复制public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
doAcquireNanos方法中,使用System.nanoTime()计算剩余时间,当剩余时间小于等于0时立即返回false。精确的时间计算对性能调优很重要,特别是在高并发场景下。
7. 性能优化与实战技巧
7.1 避免锁竞争的最佳实践
基于AQS实现高性能同步器时,有几个关键优化点:
- 减少tryAcquire/tryRelease的耗时:这些方法通常处于热点路径
- 合理设置自旋次数:在acquireQueued中,自旋检查前驱是否为head可以减少上下文切换
- 使用公平锁还是非公平锁:非公平锁通常吞吐量更高,但可能导致饥饿
7.2 调试AQS相关问题
当遇到死锁或性能问题时,可以通过以下方式诊断:
- 打印队列状态:AQS提供了toString方法显示队列内容
- 检查waitStatus:CANCELLED节点过多可能意味着超时设置不合理
- 使用jstack分析:查看线程栈中LockSupport.park的调用位置
我在实际项目中曾遇到一个典型问题:由于没有正确处理中断,导致节点停留在CANCELLED状态无法清除,最终队列越来越长。解决方案是在acquireInterruptibly中确保所有路径都能正确清理节点。
