什么是AQS?
一、AQS基本介绍
1.1 什么是AQS
AbstractQueuedSynchronizer(AQS) 是Java并发编程的基础框架,位于java.util.concurrent.locks
包下,由并发大师Doug Lea设计。它通过原子整数状态(state) 和FIFO等待队列实现了一套通用的线程同步机制,为ReentrantLock、Semaphore、CountDownLatch等同步工具提供统一的底层支持。
AQS的核心思想是模板方法模式:将复杂的线程排队、阻塞/唤醒等通用逻辑封装在抽象类中,而将同步状态的获取/释放等具体逻辑延迟到子类实现。开发者只需重写少量钩子方法(如tryAcquire
、tryRelease
),即可快速构建自定义同步器。
1.2 核心组件
AQS的实现依赖于两个核心组件:
(1)同步状态(State)
- 由
private volatile int state
变量表示,用于记录资源的占用情况。 - 通过
getState()
、setState()
和compareAndSetState()
方法操作,确保线程安全。 - 不同同步器对state的定义不同:
- ReentrantLock:state表示锁的重入次数(0=未持有,>0=持有)。
- Semaphore:state表示剩余许可数量。
- CountDownLatch:state表示倒计时计数器。
(2)CLH等待队列
- 基于双向链表实现的FIFO队列,用于存放竞争资源失败的线程。
- 每个节点(Node)包含线程引用、等待状态(waitStatus)、前驱/后继指针:
static final class Node { volatile int waitStatus; // 节点状态:CANCELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3) volatile Node prev; // 前驱节点 volatile Node next; // 后继节点 volatile Thread thread; // 关联线程 Node nextWaiter; // 条件队列或共享模式标记 }
- CLH队列的优势:将多线程对共享资源的竞争转化为对前驱节点状态的轮询,减少内存竞争(相比MCS锁更适合SMP架构)。
1.3 两种同步模式
AQS支持两种资源共享模式,可灵活适配不同场景:
模式 | 特点 | 典型实现 |
---|---|---|
独占模式 | 同一时刻仅一个线程可获取资源 | ReentrantLock、ReentrantReadWriteLock.WriteLock |
共享模式 | 多个线程可同时获取资源 | Semaphore、CountDownLatch、ReentrantReadWriteLock.ReadLock |
二、AQS源码深度解析
2.1 核心数据结构
AQS的核心字段包括:
public abstract class AbstractQueuedSynchronizer {
private transient volatile Node head; // 队列头节点
private transient volatile Node tail; // 队列尾节点
private volatile int state; // 同步状态
}
- head/tail:通过volatile修饰确保线程可见性,使用CAS操作原子更新。
- Node节点状态:
CANCELLED(1)
:线程因超时或中断取消等待。SIGNAL(-1)
:后继节点需要被唤醒。CONDITION(-2)
:节点在条件队列中等待。PROPAGATE(-3)
:共享模式下释放操作需传播。
2.2 独占模式:acquire/release流程
以ReentrantLock为例,分析独占模式的核心逻辑:
(1)获取锁:acquire(int arg)
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt(); // 中断当前线程
}
}
流程拆解:
- tryAcquire:子类实现的尝试获取锁逻辑(如ReentrantLock的公平/非公平实现)。
- addWaiter:将当前线程封装为Node节点,通过CAS加入队列尾部。
- acquireQueued:节点进入队列后自旋尝试获取锁,失败则阻塞。
关键方法acquireQueued
的自旋逻辑:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { // 自旋
final Node p = node.predecessor(); // 获取前驱节点
if (p == head && tryAcquire(arg)) { // 前驱是头节点且获取锁成功
setHead(node); // 当前节点成为新头节点
p.next = null; // 帮助GC
failed = false;
return interrupted;
}
// 判断是否需要阻塞
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
interrupted = true;
}
}
} finally {
if (failed) cancelAcquire(node); // 取消获取
}
}
(2)释放锁:release(int arg)
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:唤醒头节点的后继节点(通过
LockSupport.unpark
),确保公平性。
2.3 共享模式:acquireShared/releaseShared流程
以Semaphore为例,共享模式允许多个线程同时获取资源:
(1)获取共享资源:acquireShared(int arg)
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) { // 返回值<0表示获取失败
doAcquireShared(arg); // 进入队列等待
}
}
- tryAcquireShared:子类实现共享资源获取逻辑(如Semaphore判断剩余许可是否充足)。
(2)释放共享资源:releaseShared(int arg)
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 子类实现释放逻辑
doReleaseShared(); // 唤醒后续节点(可能传播唤醒)
return true;
}
return false;
}
- PROPAGATE状态:共享模式下,头节点释放资源后需唤醒所有等待线程(如CountDownLatch计数归0后唤醒所有等待线程)。
2.4 条件变量(ConditionObject)
AQS通过内部类ConditionObject
实现条件等待/通知机制,类似synchronized
的wait/notify
,但支持多条件队列:
public class ConditionObject implements Condition {
private transient Node firstWaiter; // 条件队列头
private transient Node lastWaiter; // 条件队列尾
}
核心方法:
await()
:释放锁并进入条件队列,被唤醒后重新竞争锁。signal()
:将条件队列头节点转移到同步队列,等待获取锁。
与Object.wait的对比:
特性 | Object.wait | Condition.await |
---|---|---|
锁依赖 | synchronized | Lock |
条件队列数量 | 1个(与锁绑定) | 多个(可创建多个Condition) |
唤醒方式 | 随机唤醒或全部唤醒 | 精确唤醒单个或全部节点 |
2.5 性能优化:自旋与阻塞
AQS通过自适应自旋减少线程阻塞开销:
- 当线程获取锁失败时,先自旋几次(基于JVM动态调整次数),若仍失败则调用
LockSupport.park
阻塞。 - 自旋的实现位于
acquireQueued
的shouldParkAfterFailedAcquire
方法中,根据前驱节点状态判断是否需要阻塞。
三、AQS使用场景与自定义同步器
3.1 适用场景
AQS适用于实现各类同步工具,典型场景包括:
- 互斥锁:如ReentrantLock,控制资源独占访问。
- 限流器:如Semaphore,限制并发访问数量。
- 倒计时器:如CountDownLatch,等待多个线程完成。
- 读写分离锁:如ReentrantReadWriteLock,优化读多写少场景。
3.2 自定义同步器示例:简易互斥锁(Mutex)
通过继承AQS实现一个简单的互斥锁:
public class Mutex implements Lock {
private final Sync sync = new Sync();
// 内部类继承AQS
private static class Sync extends AbstractQueuedSynchronizer {
// 尝试获取独占锁(state=0时CAS设为1)
@Override
protected boolean tryAcquire(int arg) {
return compareAndSetState(0, 1);
}
// 释放独占锁(state设为0)
@Override
protected boolean tryRelease(int arg) {
setState(0);
return true;
}
// 是否独占持有
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 创建条件变量
Condition newCondition() {
return new ConditionObject();
}
}
// 实现Lock接口方法
@Override public void lock() { sync.acquire(1); }
@Override public void unlock() { sync.release(1); }
@Override public Condition newCondition() { return sync.newCondition(); }
// 其他方法(tryLock、lockInterruptibly等)省略...
}
使用示例:
Mutex lock = new Mutex();
lock.lock();
try {
// 临界区操作
} finally {
lock.unlock();
}
四、JUC包中基于AQS的类
JUC包中多数同步工具均基于AQS实现,以下是核心类的分析:
4.1 ReentrantLock:可重入独占锁
- 核心原理:state表示重入次数,
tryAcquire
中判断当前线程是否为持有者,支持公平/非公平模式。 - 公平锁:
tryAcquire
时检查队列是否有前驱节点,严格按FIFO顺序获取。 - 非公平锁:直接CAS尝试获取锁,可能插队(默认模式,吞吐量更高)。
4.2 Semaphore:信号量
- 核心原理:state表示剩余许可数,
tryAcquireShared
减少许可,tryReleaseShared
增加许可。 - 应用场景:限制并发线程数(如连接池资源控制)。
4.3 CountDownLatch:倒计时器
- 核心原理:state初始化为计数N,
countDown()
递减state,await()
在state=0时唤醒所有等待线程。 - 特点:不可重置,一次性使用。
4.4 ReentrantReadWriteLock:读写锁
- 核心原理:state按位拆分,高16位表示读锁计数,低16位表示写锁计数。
- 读锁:共享模式,
tryAcquireShared
检查写锁是否释放。 - 写锁:独占模式,
tryAcquire
检查读锁和写锁是否释放。 - 锁降级:支持写锁→读锁(获取写锁后获取读锁,再释放写锁),但不支持升级。
4.5 CyclicBarrier:循环屏障
- 实现方式:基于ReentrantLock和Condition,间接使用AQS。
- 特点:计数可重置,适用于循环执行的任务阶段同步。
五、总结
5.1 AQS的优势
- 通用性:统一实现线程排队、阻塞/唤醒逻辑,简化同步器开发。
- 灵活性:支持独占/共享两种模式,可按需组合(如ReadWriteLock)。
- 高性能:CLH队列减少内存竞争,自适应自旋降低阻塞开销。
5.2 局限性
- 复杂度:直接使用AQS需理解底层原理,门槛较高。
- 单一状态:仅通过一个int state变量表示状态,复杂场景需按位拆分(如读写锁)。
5.3 实践建议
- 优先使用JUC工具:如ReentrantLock、Semaphore等,避免重复造轮子。
- 自定义同步器:仅在现有工具无法满足需求时(如特殊资源控制逻辑),基于AQS实现。
- 注意内存可见性:操作共享变量时需配合volatile或锁机制。
AQS作为Java并发编程的基石,其设计思想(如模板方法、CLH队列)和实现细节(如CAS操作、状态管理)对理解并发原理至关重要。掌握AQS不仅能帮助开发者更好地使用JUC工具,还能为自定义同步组件提供理论支撑。