一、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
方法就结束了,线程继续执行。 - 如果失败,进入下一步。
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 等无锁编程技术,减少了线程上下文切换的开销。
- 灵活强大:通过两种模式的区分,可以构建出各种复杂的同步工具。
如果小假的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!