引言
在Java并发编程中,ReentrantLock
、Semaphore
、CountDownLatch
等同步工具的高效实现,离不开一个核心框架------AQS(AbstractQueuedSynchronizer) 。作为java.util.concurrent.locks
包的核心,AQS提供了一套通用的同步器框架,将复杂的线程排队、阻塞唤醒等逻辑封装,让开发者只需实现少量方法即可构建高性能同步组件。
本文将从设计思想、底层实现到实际应用,全面解析AQS的工作原理。
一、AQS的核心设计思想
-
资源状态抽象:
state
变量-
AQS通过一个
volatile int
类型的state
字段表示共享资源的状态,其语义由子类定义:- 在
ReentrantLock
中,state
表示锁的重入次数(0表示未锁定,≥1表示被同一线程多次获取)。 - 在
Semaphore
中,state
表示剩余的许可数量。 - 在
CountDownLatch
中,state
表示倒计时的剩余计数。
- 在
-
-
线程排队机制:CLH队列变体
- AQS内部维护一个双向链表的等待队列(基于CLH锁的变体),用于管理未获取资源的线程。
- 节点(Node)结构 :每个节点保存线程引用、等待状态(
waitStatus
)及前后指针。 - 虚拟头节点 :队列头节点(
head
)不关联实际线程,仅作为占位符,简化入队和出队操作。
-
模板方法模式
-
钩子方法:子类需实现以下方法定义资源获取/释放规则:
tryAcquire(int)
:独占模式获取资源(如锁)。tryRelease(int)
:独占模式释放资源。tryAcquireShared(int)
:共享模式获取资源(如信号量)。tryReleaseShared(int)
:共享模式释放资源。
-
骨架方法 :AQS提供
acquire()
、release()
等公共逻辑,处理线程排队、阻塞唤醒等通用流程。
-
二、AQS的底层实现
1. 核心数据结构
java
public abstract class AbstractQueuedSynchronizer {
private volatile int state; // 资源状态
private transient volatile Node head; // 队列头节点
private transient volatile Node tail; // 队列尾节点
// 节点类(等待队列中的线程)
static final class Node {
volatile int waitStatus; // 等待状态(CANCELLED、SIGNAL等)
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 关联的线程
Node nextWaiter; // 条件队列或共享模式标识
}
}
2. 独占模式(以ReentrantLock为例)
-
获取锁流程(
acquire
) :- 尝试直接获取 :调用子类实现的
tryAcquire()
方法(如CAS修改state
)。 - 失败后入队 :将当前线程包装为节点,通过
addWaiter()
加入队列尾部。 - 自旋检查:在队列中自旋检查前驱节点是否为头节点,若是则再次尝试获取资源。
- 阻塞线程 :若仍失败,调用
LockSupport.park()
挂起线程。
- 尝试直接获取 :调用子类实现的
-
释放锁流程(
release
) :- 释放资源 :调用子类实现的
tryRelease()
减少state
值,若归零则清空持有线程标记。 - 唤醒后继节点 :通过
unparkSuccessor()
唤醒队列中下一个未取消的线程。
- 释放资源 :调用子类实现的
3. 共享模式(以Semaphore为例)
- 允许多个线程同时获取资源(如信号量的许可)。
tryAcquireShared()
返回剩余可用资源数(负数表示失败),唤醒后续多个等待线程。
三、AQS的关键机制
-
自旋优化
- 线程在入队前短暂自旋尝试获取资源,避免直接挂起,减少上下文切换开销。
-
中断与取消
-
节点状态(
waitStatus
) :CANCELLED (1)
:线程已取消等待(如超时或中断)。SIGNAL (-1)
:当前节点释放资源后需唤醒后继节点。CONDITION (-2)
:节点在条件队列中等待。
-
自动清理:AQS在遍历队列时会自动跳过已取消的节点。
-
-
条件变量(
ConditionObject
)-
独立条件队列 :每个
Condition
对象维护一个单向链表队列,用于实现线程的精准等待/唤醒。 -
await()
流程:- 释放锁,创建条件节点并入队。
- 调用
LockSupport.park()
挂起线程。 - 被唤醒后重新竞争锁。
-
signal()
流程:- 将条件队列中的节点转移到AQS主队列。
- 节点在主队列中等待获取锁后继续执行。
-
四、AQS与ReentrantLock的协作
-
Sync内部类
ReentrantLock
通过内部类Sync
继承AQS,并实现tryAcquire
和tryRelease
。NonfairSync
(非公平锁)和FairSync
(公平锁)分别定义不同的获取策略。
-
公平锁的实现
javaprotected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 公平锁需检查是否有前驱节点在等待 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { // 重入逻辑 setState(c + acquires); return true; } return false; }
五、常见问题解答
-
为什么AQS使用双向链表?
- 支持高效的取消操作:节点取消时需快速定位前驱和后继节点,断开链接。
-
state
变量为什么是volatile
?- 保证多线程间的可见性,确保资源状态的修改对所有线程立即可见。
-
AQS如何避免死锁?
- 不直接解决死锁,但提供超时(
tryLock
)、中断(lockInterruptibly
)等机制,依赖开发者合理使用。
- 不直接解决死锁,但提供超时(
-
AQS的性能瓶颈是什么?
- 高并发下CAS竞争激烈时可能导致CPU空转,可通过分段锁(如
ConcurrentHashMap
)或减少锁粒度优化。
- 高并发下CAS竞争激烈时可能导致CPU空转,可通过分段锁(如
六、AQS的设计哲学
-
职责分离
- AQS封装线程排队、阻塞唤醒等通用逻辑,子类仅需定义资源管理规则。
-
无锁化设计
- 通过CAS操作管理
state
和队列指针,减少锁竞争,提升性能。
- 通过CAS操作管理
-
可扩展性
- 支持独占/共享模式、公平/非公平策略,适配各类同步场景。
七、总结
AQS是Java并发编程的"基石",其价值体现在:
- 标准化同步逻辑:线程排队、状态管理、中断处理等复杂流程被封装。
- 高性能:通过自旋、CAS、CLH队列等机制最大化吞吐量。
- 灵活性:模板方法模式支持快速实现锁、信号量、栅栏等同步工具。