一、aqs 是什么?
aqs,全称 abstractqueuedsynchronizer,即抽象队列同步器。
- 抽象:它是一个抽象类,本身不能直接实例化,需要子类去继承它,并实现其保护方法来管理同步状态。
- 队列:它内部维护了一个先进先出(fifo)的等待队列,用于存放那些没有抢到锁的线程。
- 同步器:它是构建锁和其他同步组件(如 semaphore、countdownlatch 等)的基础框架。
核心思想:
aqs 使用一个整型的 volatile 变量(state) 来表示同步状态(例如,锁被重入的次数、许可的数量等),并通过一个内置的 fifo 队列来完成资源获取线程的排队工作。
设计模式:
aqs 是 模板方法模式 的经典应用。父类(aqs)定义了骨架和核心算法,而将一些关键的操作以 protected 方法的形式留给子类去实现。这样,实现一个自定义同步器只需要关注如何管理 state 状态即可,至于线程的排队、等待、唤醒等复杂操作,aqs 已经帮我们完成了。
二、aqs 的核心结构
aqs 的核心可以概括为三部分:同步状态(state)、等待队列 和 条件队列。
1. 同步状态(state)
这是一个 volatile int 类型的变量,是 aqs 的灵魂。
private volatile int state;
它的具体含义由子类决定,非常灵活:
- 在 reentrantlock 中,
state表示锁被同一个线程重复获取的次数。state=0表示锁空闲,state=1表示锁被占用,state>1表示锁被重入。 - 在 semaphore 中,
state表示当前可用的许可数量。 - 在 countdownlatch 中,
state表示计数器当前的值。
对 state 的操作是原子的,通过 getstate(), setstate(int newstate), compareandsetstate(int expect, int update) 等方法进行。
2. 等待队列(clh 队列的变体)
这是一个双向链表,是 aqs 实现阻塞锁的关键。当线程请求共享资源失败时,aqs 会将当前线程以及等待状态等信息构造成一个节点(node) 并将其加入队列的尾部,同时阻塞该线程。
- 头节点(head):指向获取到资源的线程所在的节点。头节点不持有线程,是一个“虚节点”。
- 尾节点(tail):指向队列中最后一个节点。
当一个线程释放资源时,它会唤醒后继节点,后继节点成功获取资源后,会将自己设置为新的头节点。
主要原理图如下:

aqs 使用一个 volatile 的 int 类型的成员变量来表示同步状态,通过内置的 fifo 队列来完成资源获取的排队工作,通过 cas 完成对 state 值的修改。
3. 条件队列(condition object)
aqs 内部类 conditionobject 实现了 condition 接口,用于支持 await/signal 模式的线程间协作。每个 conditionobject 对象都维护了一个自己的单向链表(条件队列)。
- 当线程调用
condition.await()时,会释放锁,并将当前线程构造成节点加入条件队列,然后阻塞。 - 当线程调用
condition.signal()时,会将条件队列中的第一个等待节点转移到 aqs 的等待队列中,等待重新获取锁。
注意:一个 aqs 实例可以对应多个 condition 对象(即多个条件队列),但只有一个等待队列。
三、aqs 的设计与关键方法
aqs 将资源获取的方式分为两种:
- 独占模式(exclusive):一次只有一个线程能执行,如 reentrantlock。
- 共享模式(shared):多个线程可以同时执行,如 semaphore、countdownlatch。
aqs 提供了顶层的入队和出队逻辑,而将尝试获取资源和尝试释放资源的具体策略留给了子类。
需要子类重写的关键方法(protected)
这些方法在 aqs 中是 protected 的,默认抛出 unsupportedoperationexception。
独占模式:
boolean tryacquire(int arg):尝试以独占方式获取资源。成功返回 true,失败返回 false。boolean tryrelease(int arg):尝试以独占方式释放资源。成功返回 true,失败返回 false。
共享模式:
int tryacquireshared(int arg):尝试以共享方式获取资源。负数表示失败;0 表示成功,但后续共享获取可能失败;正数表示成功,且后续共享获取可能成功。boolean tryreleaseshared(int arg):尝试以共享方式释放资源。
其他:
boolean isheldexclusively():当前同步器是否在独占模式下被线程占用。在 condition 相关操作中会用到。
供外部调用的重要方法(public)
这些是模板方法,子类一般不重写,使用者(或子类)直接调用。
独占模式:
void acquire(int arg):以独占模式获取资源,忽略中断。如果获取失败,会进入等待队列。void acquireinterruptibly(int arg):同上,但响应中断。boolean tryacquirenanos(int arg, long nanostimeout):在acquireinterruptibly基础上增加了超时限制。boolean release(int arg):以独占模式释放资源。
共享模式:
void acquireshared(int arg):以共享模式获取资源。void acquiresharedinterruptibly(int arg):响应中断的共享获取。boolean tryacquiresharednanos(int arg, long nanostimeout):带超时的共享获取。boolean releaseshared(int arg):以共享模式释放资源。
四、源码级工作流程解析(以acquire为例)
我们来看一下最核心的 acquire 方法,它展示了 aqs 的完整工作流程:
public final void acquire(int arg) {
if (!tryacquire(arg) && // 1. 尝试直接获取资源(子类实现)
acquirequeued(addwaiter(node.exclusive), arg)) // 2. 获取失败,则加入队列;3. 在队列中自旋/阻塞等待
selfinterrupt(); // 如果在等待过程中被中断,补上中断标记
}tryacquire(arg):- 这是子类实现的方法。比如在 reentrantlock 的非公平锁实现中,它会直接尝试使用 cas 修改
state,如果成功,就将当前线程设置为独占线程。 - 如果
tryacquire成功,整个acquire方法就结束了,线程继续执行。 - 如果失败,进入下一步。
- 这是子类实现的方法。比如在 reentrantlock 的非公平锁实现中,它会直接尝试使用 cas 修改
addwaiter(node.exclusive):- 创建一个代表当前线程的 node 节点,模式为独占模式(node.exclusive)。
- 通过 cas 操作,快速地将这个新节点设置为尾节点。如果失败,则进入
enq(node)方法,通过自旋 cas 的方式确保节点被成功添加到队列尾部。
acquirequeued(final node node, int arg):- 这是核心中的核心。节点入队后,会在这个方法里进行自旋(循环)等待。
- 在循环中,它会检查自己的前驱节点是不是头节点(
p == head)。如果是,说明自己是队列中第一个等待的线程,会再次调用tryacquire尝试获取资源(因为此时锁可能刚好被释放了,这是一个避免不必要的线程挂起、提高性能的优化)。 - 如果获取成功,就将自己设为新的头节点,然后返回。
- 如果前驱不是头节点,或者再次尝试获取失败,则会调用
shouldparkafterfailedacquire方法,检查并更新前驱节点的状态(比如将其waitstatus设置为signal,表示“当你释放锁时,需要唤醒我”)。 - 如果一切就绪,就调用
parkandcheckinterrupt()方法,使用locksupport.park(this)阻塞(挂起)当前线程。 - 当线程被唤醒后(通常是由前驱节点释放锁时
unpark的),会再次检查自己是否是头节点的后继,并重复上述自旋过程,直到成功获取资源。
selfinterrupt():- 如果在等待过程中线程被中断,
acquirequeued方法会返回true,这里会调用selfinterrupt补上中断标志,因为 aqs 在acquire过程中是忽略中断的。
- 如果在等待过程中线程被中断,
释放流程(release)相对简单:
public final boolean release(int arg) {
if (tryrelease(arg)) { // 1. 子类尝试释放资源
node h = head;
if (h != null && h.waitstatus != 0)
unparksuccessor(h); // 2. 唤醒后继节点
return true;
}
return false;
}unparksuccessor 会找到队列中第一个需要唤醒的线程(通常是头节点的下一个有效节点),然后调用 locksupport.unpark(s.thread) 将其唤醒。
五、aqs 的应用举例
aqs 是 juc 包的基石,几乎所有的同步工具都基于它:
- reentrantlock:使用 aqs 的独占模式,
state表示重入次数。 - reentrantreadwritelock:读写锁。aqs 的
state高16位表示读锁状态,低16位表示写锁状态。 - semaphore:使用 aqs 的共享模式,
state表示可用许可数。 - countdownlatch:使用 aqs 的共享模式,
state表示计数器值。countdown()是releaseshared,await()是acquireshared。 - threadpoolexecutor:其内部的工作线程
worker类,也继承了 aqs,用于实现独占锁,来判断线程是否空闲。
六、总结
aqs 的核心贡献在于,它提供了一个强大的框架,将复杂的线程排队、阻塞、唤醒等底层操作封装起来,让同步器的开发者只需要关注一个核心问题:如何管理那个 state 变量。
它的优点:
- 极大地降低了构建锁和同步器的复杂度。
- 性能高效:通过自旋、cas 等无锁编程技术,减少了线程上下文切换的开销。
- 灵活强大:通过两种模式的区分,可以构建出各种复杂的同步工具。
到此这篇关于java中的aqs入门攻略的文章就介绍到这了,更多相关java aqs是什么内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论