AQS 抽象队列同步器 资源竞争-排队等待
目录
一、AQS的核心作用
二、AQS的核心组件
1. 同步状态(state)
2. 等待队列(CLH队列)
节点状态(waitStatus)的关键值:
三、AQS的两种工作模式
1. 独占模式(Exclusive Mode)
独占模式的获取流程(以acquire(1)为例):
独占模式的释放流程(以release(1)为例):
2. 共享模式(Shared Mode)
共享模式的获取流程(以acquireShared(1)为例):
共享模式的释放流程(以releaseShared(1)为例):
四、AQS的模板方法与钩子方法
1. 模板方法(AQS已实现,子类直接使用):
2. 钩子方法(子类按需实现,默认抛出异常):
五、基于AQS的典型同步工具
六、总结
AQS(AbstractQueuedSynchronizer,抽象队列同步器)是Java并发包(java.util.concurrent
)的核心组件,是实现几乎所有同步工具(如ReentrantLock
、Semaphore
、CountDownLatch
等)的基础框架。它通过统一的模板方法和钩子方法,简化了同步器的开发,同时保证了高效的并发控制。
一、AQS的核心作用
AQS的本质是提供了一种“资源竞争-排队等待”的通用机制:
- 当线程竞争共享资源时,通过原子操作(CAS)尝试获取资源;
- 若获取失败,线程会被包装成节点(Node),加入一个FIFO(先进先出)的等待队列中阻塞等待;
- 当资源被释放时,队列中的线程会按顺序被唤醒,再次尝试获取资源。
简单说,AQS就像一个“交通指挥系统”:资源是“路口”,线程是“车辆”,AQS负责指挥车辆“抢道”和“排队”。
二、AQS的核心组件
AQS的功能依赖两个核心要素:同步状态(state) 和 等待队列(CLH队列)。
1. 同步状态(state)
- 定义:AQS用一个
volatile int state
变量表示共享资源的状态(如“是否被占用”“剩余资源数量”等),其值的具体含义由子类(同步工具)定义。 - 原子性保证:state的修改通过CAS操作(
compareAndSetState
)保证原子性,结合volatile
的可见性,确保多线程对state的操作是线程安全的。 - 示例:
-
- 在
ReentrantLock
中,state=0表示锁未被占用,state>0表示被占用(值为重入次数); - 在
Semaphore
中,state表示剩余的许可数量(可同时访问的线程数)。
- 在
2. 等待队列(CLH队列)
当线程获取资源失败时,会被封装成一个Node节点,加入到FIFO的等待队列中。队列的结构类似链表,每个节点包含以下核心信息:
Thread thread
:当前等待的线程;int waitStatus
:节点状态(决定线程是否需要唤醒、是否已取消等);Node prev
/Node next
:前驱和后继节点(构成双向链表)。
节点状态(waitStatus)的关键值:
- CANCELLED(1):节点对应的线程已取消等待(如超时或被中断),会从队列中移除;
- SIGNAL(-1):当前节点的后继节点需要被唤醒(当前节点释放资源后,需通知后继节点);
- CONDITION(-2):节点处于条件队列中(用于
Condition
接口); - PROPAGATE(-3):共享模式下,资源释放会向后传播(唤醒多个节点);
- 0:初始状态。
三、AQS的两种工作模式
AQS支持两种资源访问模式,子类可根据需求选择实现:独占模式和共享模式。
1. 独占模式(Exclusive Mode)
- 特点:资源只能被一个线程独占(如锁),其他线程必须等待。
- 核心操作:
-
- 获取资源:
acquire(int arg)
(模板方法); - 释放资源:
release(int arg)
(模板方法)。
- 获取资源:
独占模式的获取流程(以acquire(1)
为例):
- 调用
tryAcquire(1)
(钩子方法,子类实现):尝试获取资源(如修改state)。
-
- 若成功,直接返回(线程获取到资源);
- 若失败,进入下一步。
- 失败的线程被包装成Node节点,通过
addWaiter(Node.EXCLUSIVE)
加入等待队列尾部。 - 调用
acquireQueued(Node node, int arg)
:节点在队列中“自旋+阻塞”,直到被前驱节点唤醒后,再次尝试tryAcquire
。
-
- 若期间线程被中断,会记录中断状态,待获取资源后再处理(不响应中断,除非用
acquireInterruptibly
)。
- 若期间线程被中断,会记录中断状态,待获取资源后再处理(不响应中断,除非用
独占模式的释放流程(以release(1)
为例):
- 调用
tryRelease(1)
(钩子方法,子类实现):尝试释放资源(如修改state)。
-
- 若释放成功,进入下一步;
- 若失败,直接返回。
- 唤醒等待队列中“最前面”的有效节点(通过
unparkSuccessor
方法,唤醒后继节点的线程)。
2. 共享模式(Shared Mode)
- 特点:资源可被多个线程同时获取(如信号量、计数器)。
- 核心操作:
-
- 获取资源:
acquireShared(int arg)
(模板方法); - 释放资源:
releaseShared(int arg)
(模板方法)。
- 获取资源:
共享模式的获取流程(以acquireShared(1)
为例):
- 调用
tryAcquireShared(1)
(钩子方法,子类实现):尝试获取资源(返回值含义:>0表示成功且有剩余资源;0表示成功但无剩余;<0表示失败)。
-
- 若返回值≥0,获取成功,直接返回;
- 若返回值<0,进入下一步。
- 失败的线程被包装成Node节点,通过
addWaiter(Node.SHARED)
加入等待队列。 - 调用
doAcquireShared(Node node)
:节点在队列中阻塞等待,被唤醒后再次尝试tryAcquireShared
。若成功,会继续唤醒后续节点(因为共享模式允许多个线程获取)。
共享模式的释放流程(以releaseShared(1)
为例):
- 调用
tryReleaseShared(1)
(钩子方法,子类实现):尝试释放资源(如增加state)。
-
- 若释放成功,进入下一步;
- 若失败,直接返回。
- 唤醒等待队列中的节点(可能唤醒多个,因为共享模式下多个线程可同时获取)。
四、AQS的模板方法与钩子方法
AQS通过模板方法定义了同步操作的整体流程(如获取/释放资源的步骤),并将具体的资源竞争逻辑(如何修改state)交给子类通过钩子方法实现。这种设计既保证了流程的统一性,又保留了灵活性。
1. 模板方法(AQS已实现,子类直接使用):
- 独占模式:
acquire()
、acquireInterruptibly()
(响应中断)、tryAcquireNanos()
(超时)、release()
; - 共享模式:
acquireShared()
、acquireSharedInterruptibly()
、tryAcquireSharedNanos()
、releaseShared()
。
2. 钩子方法(子类按需实现,默认抛出异常):
- 独占模式:
-
tryAcquire(int arg)
:尝试获取独占资源(返回true
表示成功);tryRelease(int arg)
:尝试释放独占资源(返回true
表示成功)。
- 共享模式:
-
tryAcquireShared(int arg)
:尝试获取共享资源(返回值≥0表示成功);tryReleaseShared(int arg)
:尝试释放共享资源(返回true
表示成功)。
- 其他:
isHeldExclusively()
(判断当前线程是否独占资源,用于Condition
)。
五、基于AQS的典型同步工具
AQS是Java并发工具的“基石”,以下是常见实现:
同步工具 | 模式 | state含义 | 核心逻辑 |
| 独占模式 | 锁的重入次数(0=未占用) |
|
| 共享模式 | 剩余许可数量 |
|
| 共享模式 | 计数器值(初始为N) |
|
| 混合模式 | 高16位=读锁计数,低16位=写锁重入次数 | 读锁(共享):增加读计数;写锁(独占):抢占并设置写计数。 |
六、总结
AQS通过“同步状态(state)+ 等待队列(CLH队列)”的核心设计,结合CAS原子操作和模板方法模式,为同步工具提供了高效、通用的实现框架。其核心价值在于:
- 统一了同步工具的竞争与等待逻辑,减少重复开发;
- 基于FIFO队列和CAS,保证了并发控制的高效性和公平性(可选);
- 支持独占和共享两种模式,覆盖了绝大多数同步场景。
理解AQS是掌握Java并发编程的关键,无论是使用现有工具还是自定义同步器,都需要深入理解其“资源竞争-排队唤醒”的底层逻辑。