1. Java并发基石:AQS深度解析
在Java并发编程的世界里,AbstractQueuedSynchronizer(简称AQS)就像是一座隐藏在水面下的冰山,支撑着java.util.concurrent包中众多同步工具类的运行。作为Doug Lea大师的杰作,AQS通过精妙的设计将复杂的线程同步问题抽象为一个可重用的框架。
1.1 AQS的核心价值
AQS的核心价值在于它解决了并发编程中的三个关键问题:
- 状态管理:通过一个volatile的int类型state变量,表示同步状态
- 线程排队:采用CLH变体队列管理竞争失败的线程
- 阻塞唤醒:基于LockSupport实现高效的线程挂起与唤醒
这种设计使得开发者只需要关注如何定义state的含义和状态转换规则,就能构建出各种复杂的同步器。比如:
- ReentrantLock用state表示锁的重入次数
- Semaphore用state表示剩余的许可数量
- CountDownLatch用state表示剩余的计数
实际开发中,大约90%的同步需求都可以通过AQS的子类实现,这也是为什么它被称为Java并发编程的"基石"。
1.2 AQS的架构全景
AQS的架构可以概括为"一个状态,两个队列":
java复制// 核心字段
private volatile int state; // 同步状态
private transient volatile Node head; // 队列头
private transient volatile Node tail; // 队列尾
private transient Thread exclusiveOwnerThread; // 独占模式下的持有线程
**状态(state)**的语义由子类定义,AQS只提供原子性的修改方法。队列采用CLH锁的变体实现,这是一种高效且无饥饿的队列策略。
2. AQS核心机制详解
2.1 同步状态(state)的运作原理
state是AQS的灵魂所在,它的几个关键特性:
- volatile保证可见性:所有线程都能立即看到state的最新值
- CAS保证原子性:通过Unsafe.compareAndSwapInt实现无锁更新
- 语义由子类定义:AQS不关心state的具体含义
以ReentrantLock为例,state的工作方式:
- 0表示锁未被占用
- 1表示锁被占用
-
1表示锁被重入的次数
java复制// ReentrantLock中state的使用示例
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) throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
2.2 CLH队列的精妙设计
AQS的等待队列是CLH锁的变体,具有以下特点:
- 双向链表结构:每个节点(Node)都有prev和next指针
- 虚拟头节点:队列初始化时创建一个dummy节点
- 惰性构建:只有竞争发生时才会真正构建队列
节点状态(waitStatus)的几种取值:
- CANCELLED(1):线程已取消
- SIGNAL(-1):后继节点需要被唤醒
- CONDITION(-2):节点在条件队列中
- PROPAGATE(-3):共享模式下传播唤醒
java复制static final class Node {
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter; // 用于条件队列
}
2.3 独占模式与共享模式
AQS支持两种资源访问模式:
独占模式(Exclusive):
- 同一时刻只有一个线程能访问资源
- 典型实现:ReentrantLock
- 唤醒策略:单点唤醒,只唤醒头节点的后继
共享模式(Shared):
- 多个线程可以同时访问资源
- 典型实现:Semaphore、CountDownLatch
- 唤醒策略:级联唤醒,可能同时唤醒多个线程
模式选择的差异主要体现在Node节点的模式标记和唤醒逻辑上:
java复制// 节点模式判断
final boolean isShared() {
return nextWaiter == SHARED;
}
// 共享模式唤醒传播
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // 重试
unparkSuccessor(h); // 唤醒后继
}
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // 重试
}
if (h == head) break; // 头节点未变化则退出
}
}
3. AQS关键流程剖析
3.1 获取资源(acquire)流程
独占模式下的获取资源流程是AQS最复杂的部分,我们以ReentrantLock为例:
- 快速尝试:先调用tryAcquire尝试直接获取锁
- 入队准备:失败后调用addWaiter将当前线程包装成Node加入队尾
- 自旋检查:在队列中自旋检查是否轮到自己
- 阻塞等待:确认需要等待后调用park挂起线程
java复制public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
addWaiter的精妙之处:
- 先尝试快速入队(一次CAS)
- 失败后进入enq方法自旋入队
- 保证队列始终有dummy节点,避免边界条件判断
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;
}
3.2 释放资源(release)流程
释放资源相对简单,但有几个关键点:
- 状态恢复:调用tryRelease尝试释放资源
- 唤醒后继:成功后检查头节点状态,唤醒后继线程
- 传播机制:共享模式下会触发级联唤醒
java复制public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
unparkSuccessor的细节:
- 从tail向前遍历,确保不会漏掉新入队的节点
- 只唤醒离head最近的有效节点
- 被唤醒的线程会继续执行acquireQueued中的循环
java复制private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0) compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0) s = t;
}
if (s != null) LockSupport.unpark(s.thread);
}
4. ConditionObject深度解析
4.1 条件队列与同步队列
AQS中的ConditionObject实现了条件变量功能,它维护了一个独立的条件队列:
- 同步队列:等待获取锁的线程
- 条件队列:调用await()等待条件的线程
两者之间的转换通过signal/signalAll触发:
java复制// await()基本流程
public final void await() throws InterruptedException {
if (Thread.interrupted()) throw new InterruptedException();
Node node = addConditionWaiter(); // 加入条件队列
int savedState = fullyRelease(node); // 完全释放锁
while (!isOnSyncQueue(node)) {
LockSupport.park(this); // 挂起等待
// 处理中断...
}
// 被signal后重新竞争锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 清理工作...
}
4.2 signal的精确唤醒
与Object.notify不同,Condition.signal提供了更精确的唤醒控制:
- 不立即唤醒:只是将节点从条件队列移到同步队列
- 保持顺序:先await的线程会先被signal
- 可中断:支持响应中断的等待
java复制public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
if ((firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
5. AQS实战应用
5.1 自定义同步器示例
下面演示如何基于AQS实现一个简单的二元闭锁:
java复制class BinaryLatch {
private static class Sync extends AbstractQueuedSynchronizer {
protected int tryAcquireShared(int acquires) {
return (getState() == 1) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
setState(1);
return true;
}
}
private final Sync sync = new Sync();
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public void release() {
sync.releaseShared(1);
}
}
5.2 性能优化技巧
在实际使用AQS时,有几个性能优化点值得注意:
- 减少CAS竞争:合理设计state语义,减少状态变更频率
- 避免过早入队:在tryAcquire中先做快速尝试
- 合理选择模式:读多写少场景考虑共享模式
- 注意内存占用:长时间运行的队列可能积累大量节点
6. AQS的局限与替代方案
虽然AQS功能强大,但在某些场景下也存在局限:
- 不可中断的等待:某些操作不支持中断
- 功能扩展性:复杂的同步需求可能需要组合多个AQS
- 性能瓶颈:极端竞争下队列维护开销较大
在这些场景下,可以考虑以下替代方案:
- StampedLock:乐观读机制
- CompletableFuture:异步编程模型
- 响应式流:背压支持
7. 最佳实践与常见陷阱
7.1 最佳实践
- 优先使用现有实现:除非有特殊需求,否则应优先使用JUC提供的同步器
- 合理设置超时:避免死锁,使用tryAcquireNanos等带超时的方法
- 注意重入性:实现可重入锁时要正确处理state的累加
7.2 常见陷阱
- 内存泄漏:忘记释放资源会导致队列节点堆积
- 活锁风险:不合理的自旋策略可能导致CPU空转
- 公平性误解:非公平锁虽然吞吐高但可能导致饥饿
java复制// 典型错误示例:不正确的tryRelease实现
protected boolean tryRelease(int releases) {
// 错误:没有检查当前线程是否是锁持有者
setState(0);
return true;
}
8. AQS设计哲学启示
AQS的设计体现了几个重要的软件工程原则:
- 模板方法模式:将不变的部分封装,可变部分留给子类实现
- 关注点分离:状态管理与线程调度解耦
- 最小惊讶原则:行为可预测,符合开发者直觉
- 性能优先:无锁操作、惰性初始化等优化手段
这些设计思想不仅适用于同步器开发,也值得我们在设计其他系统时借鉴。