AQS
AQS 全称是 AbstractQueuedSynchronizer。顾名思义就是一个抽象的(可被继承复用),内部存在排队(竞争资源的线程排队)的同步器(对共享资源和线程进行同步管理)。
属性
首先来看 AQS 的成员属性。
private volatile int state;
state 是用于判断共享资源是否正在被占用的标记位,volatile 保证了线程之间的可见性(可见性就是当一个线程修改了 state 的值,其他线程下一次读取都能读到最新值)。
为什么 state 的类型是 int?而不是 boolean?
线程获取锁有两种模式,独占和共享。当一个线程以独占模式获取锁时,其他线程任何必须等待,而当一个线程以共享模式获取锁时,其他也想以共享模式获取锁的线程也能够一起访问共享资源,但其他想以独占模式获取锁的线程需要等待。所以,共享模式下,可能有多个线程正在共享资源,所以 state 需要表示线程占用数量,因此是 int 值。
private transient volatile Node head;
private transient volatile Node tail;
AQS 中存在一个队列用于对等待线程进行管理,这个队列通过一个 FIFO 的双向链表来实现,head 和 tail 变量表示这个队列的头尾。
内部类
队列中每个节点的类型是内部类 Node。
static final class Node {
// 共享模式
static final Node SHARED = new Node();
// 独占模式
static final Node EXCLUSIVE = null;
// 节点已被取消(线程超时或中断)
static final int CANCELLED = 1;
// 当前节点的后继节点需要被唤醒(unpark)
static final int SIGNAL = -1;
// 当前节点在条件队列中等待
static final int CONDITION = -2;
// 共享模式下,释放操作需要传播到后续节点
static final int PROPAGATE = -3;
// 线程的等待状态
volatile int waitStatus;
// 前驱节点
volatile Node prev;
// 后继节点
volatile Node next;
// 节点对应的线程
volatile Thread thread;
// 指向下一个等待节点
Node nextWaiter;
// 判断节点是否在共享模式下等待
final boolean isShared() {
return nextWaiter == SHARED;
}
// 获取前驱节点
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
队列里的节点有两种模式,独占和共享。
这里重点关注的是 waitStatus 这个属性,它是一个枚举值,AQS 工作时必然伴随着 Node 的 waitStatus 值的变化,如果理解了 waitStatus 的变化时机,对理解 AQS 整个工作原理有很大帮助。
waitStatus 主要包含四个状态:
- 0,节点初始化默认值活节点已经释放锁
- CANCELLED 为 1,表示当前节点获取锁的请求已经被取消了
- SIGNAL 为 -1,表示当前节点的后续节点需要被唤醒
- CONDITION 为 -2,表示当前节点正在等待某一个 Condition 对象,和条件模式相关
- PROPAGATE 为 -3,传递共享模式下锁释放状态,和共享模式相关
方法
有两种使用场景:
- 尝试获取锁,不管有没有获取到,立即返回。
- 必须获取锁,如果当前时刻锁被占用,则进行等待。
// try acquire
protected boolean tryAcquire(int arg){
throw new UnsupportedOperationException();
}
// acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire
是一个被 protected 修饰的方法,参数是一个 int 值,代表对 int state 的增加操作,返回值是 boolean,代表是否成功获得锁。
该方法只有一行实现 throw new UnsupportedOperationException()
,意图很明显,AQS 规定继承类必须 override tryAcquire
方法,否则直接抛出 UnsupportedOperationException()
。为什么这里一定要上层自己实现呢?因为尝试获取锁这个操作中可能包含某些业务自定义的逻辑,比如是否“可重入”等。
若上层调用 tryAcquire
返回 true,线程获得锁,此时可以对响应的共享资源进行操作,使用完之后再进行释放。如果调用 tryAcquire
返回 false,且上层逻辑上不想等待锁,那么可以自己进行相应的处理;如果上层逻辑选择等待锁,那么可以直接调用 acquire
方法,acquire
方法内部封装了复杂的排队处理逻辑,非常易用。
acquire
被 final 修饰,表示不允许子类擅自 override。
//acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
if 条件包含了两部分:
!tryAcquire(arg)
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
这里的意思是,如果 tryAcquire
获取锁成功,那么 !tryAcquire
为 false,说明已经获取锁,根本不用参与排队,也就不用再执行后续判断条件。根据判断条件的短路规则,直接返回。
如果 tryAcquire
返回 false,说明需要排队,那么就进而执行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
,acquireQueued
方法其中嵌套了 addWaiter
方法。
private Node addWaiter(Node mode) {
// 创建一个新节点,mode 可以是 Node.EXCLUSIVE(独占模式)或 Node.SHARED(共享模式)
Node node = new Node(Thread.currentThread(), mode);
// 尝试快速将节点添加到队列尾部
Node pred = tail;
if (pred != null) {
node.prev = pred; // 设置新节点的前驱为当前尾节点
if (compareAndSetTail(pred, node)) { // CAS 设置新节点为尾节点
pred.next = node; // 将当前尾节点的后继指向新节点
return node;
}
}
// 如果快速添加失败,调用 enq 方法进行完整入队操作
enq(node);
return node;
}
顾名思义,这个方法的作用就是将当前线程封装成一个 Node
,然后加入等待队列,返回值即为该 Node
。这段主要逻辑就是,首先创建一个 Node
对象,我们需要将其插入队尾。但是我们需要考虑多线程场景,即假设存在多个线程正在调用 addWaiter
方法。
新建 pred
节点引用,指向当前的尾节点,如果尾节点不为空,那么下面将进行三步操作:
- 将当前节点的
pre
指针指向pred
节点(尾节点)。 - 尝试通过 CAS 操作将当前节点置为尾节点。
- 如果返回 false,说明
pred
节点已经不是尾节点,在上面的执行过程中,尾节点已经被其他线程修改,那么退出判断,调用enq
方法,准备重新进入队列。 - 如果返回 true,说明 CAS 操作之前,
pred
节点依旧是尾节点,CAS 操作使当前 node 顺利成为尾节点。若当前 node 顺利成为尾节点,那么pred
节点和当前 node 之间的相对位置已经确定,此时将pred
节点的next
指针指向当前 node,是不会存在线程安全问题的。
- 如果返回 false,说明
由于在多线程环境下执行,这里存在三个细节,也是该方法中的重点。
某线程执行到第 8 行时,pred
引用指向的对象可能已经不再是尾节点,所以 CAS 失败;
如果 CAS 成功,诚然 CAS 操作是具有原子性的,但是 9、10 两行在执行时并不具备原子性,只不过此时 pred
节点和当前 node 之间的相对位置已经确定,其他线程只是正在插入新的尾节点,并不会影响到这里的操作,所以是线程安全的。
需要记住的是,当前后两个节点建立连接的时候,首先是后节点的 pre
指向前节点,当后节点成功成为尾节点后,前节点的 next
才会指向后节点。
如果理解了这些,我们再来看第 14 行。当程序运行到这一行,说明出现了两种情况之一:
- 队列为空
- 快速插入失败,想要进行完整流程的插入,这里所说的快速插入,指的就是 6-12 行的逻辑,当并发线程较少的情况下,快速插入成功率很高,程序不用进入完整流程插入,效率会更高。
既然程序来到了第 14 行,那么我们就来看看完整流程的插入是什么样子的。
private Node enq(final Node node) {
for (;;) { // 自旋,直到成功
Node t = tail;
if (t == null) { // 如果队列为空,初始化队列
if (compareAndSetHead(new Node())) // 创建一个空节点作为头节点
tail = head; // 头节点和尾节点指向同一个空节点
} else {
node.prev = t; // 设置新节点的前驱为当前尾节点
if (compareAndSetTail(t, node)) { // CAS 设置新节点为尾节点
t.next = node; // 将当前尾节点的后继指向新节点
return t;
}
}
}
}
这个方法的逻辑就是在最外层加上了一层死循环,如果队列未初始化(tail == null
),那么就尝试初始化,如果尾节点插入失败,那么就不断重试,直到插入成功为止。
一旦 addWaiter
成功之后,AQS 在各个线程中维护了当前 Node
的 waitStatus
,根据不同的状态,程序来做出不同的操作。通过调用 acquireQueue
方法,开始对 Node
的 waitStatus
进行跟踪维护。
//acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
继续看 acquireQueue
源码。
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; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node); // 如果失败,取消节点
}
}
首先,acquireQueued
方法内定义了一个局部变量 failed
,初始值为 true
,意思是默认失败。还有一个变量 interrupted
,初始值为 false
,意思是等待锁的过程中当前线程没有被中断。再来看看在整个方法中,哪里用到了这两个变量?
- 第 11 行,
return
之前,failed
值会改为false
,代表执行成功,并且返回interrupted
值。 - 第 15 行,如果满足判断条件,
interrupted
将会被改为true
,最终在第 11 行被返回出去。 - 第 18 行,
finally
块中,通过判断failed
值来进行一个名为cancelAcquire
的操作,即取消当前线程获取锁的行为。
那么我们基本可以将 acquireQueued
分为三部分。
- 7-11 行:当前置节点为 head,说明当前节点有权限去尝试拿锁,这是一种约定。如果
tryAcquire(arg)
返回 true,代表拿到了锁,那么顺理成章,函数返回。如果不满足第 7 行的条件,那么进入下一阶段。 - 13-15 行:
if
中包含两个方法,看名字(详细方法体后续再看)是首先判断当前线程是否需要挂起等待?如果需要,那么就挂起,并且判断外部是否调用线程中断;如果不需要,那么继续尝试拿锁。 - 18-19 行:如果
try
块中抛出非预期异常,那么取消当前线程获取锁的行为。
这里有三点需要着重关注一下。
- 一个约定:head 节点代表当前正在持有锁的节点。若当前节点的前置节点是 head,那么该节点就开始自旋地获取锁。一旦 head 节点释放,当前节点就能第一时间获取到。
shouldParkAfterFailedAcquire
和parkAndCheckInterrupt
方法体细节。interrupted
变量最终被返回出去后,上层acquire
方法判断该值,来选择是否调用当前线程中断。这里属于一种延迟中断机制。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 前驱节点状态为 SIGNAL,表示它会通知后续节点,当前线程可以安全挂起
return true;
if (ws > 0) {
// 前驱节点状态为 CANCELLED(>0),需跳过这些节点,直到找到未取消的前驱
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 前驱节点状态为 0 或 PROPAGATE,需将其状态设置为 SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
若当前节点没有拿锁的权限或拿锁失败,那么将会进入 shouldParkAfterFailedAcquire
判断是否需要挂起(park),方法的参数是 pred Node
和当前 Node
的引用。
- 首先获取
pred Node
的waitStatus
,若pred
的waitStatus
为SIGNAL
,说明前置节点也在等待拿锁,并且之后会唤醒当前节点,所以当前线程可以挂起休息,返回true
。 - 如果
ws > 0
,说明pred
的waitStatus
是CANCEL
,所以可以将其从队列中删除。这里通过从后向前搜索,将pred
指向搜索过程中第一个waitStatus
为非CANCEL
的节点。相当于链式地删除被CANCEL
的节点。然后返回false
,代表当前节点不需要挂起,因为pred
指向了新的 Node,需要重试外层的逻辑。 - 除此之外,
pred
的 ws 还有两种可能,0
或PROPAGATE
,有人可能会问,为什么不可能是CONDITION
? 因为waitStatus
只有在其他条件模式下,才会被修改为CONDITION
,这里不会出现,并且只有在共享模式下,才可能出现waitstatus
为PROPAGATE
,暂时也不用管。那么在独占模式下 ws 在这里只会出现0
的情况。0
代表pred
处于初始化默认状态,所以通过 CAS 将当前pred
的waitStatus
修改为SIGNAL
,然后返回false
,重试外层逻辑。
这个方法开始涉及到对 Node 的 waitSatus
的修改,相对比较关键。
如果 shouldParkAfterFailedAcquire
返回 false
,那么再进行一轮重试;如果返回 true
,代表当前节点需要被挂起,则执行 parkAndCheckInterrupt
方法。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 挂起当前线程
return Thread.interrupted(); // 返回线程的中断状态,并清除中断标志
}
这个方法只有两行,对当前线程进行挂起的操作。这里 LockSupport.park(this)
本质是通过 UNSAFE 下的 native 方法调用操作系统原语来将当前线程挂起。
此时当前 Node
中的线程将阻塞在此处,直到持有锁的线程调用 release
方法,release
方法会唤醒后续节点。
那这边的 return Thread.interrupted()
又是什么意思呢? 这是因为在线程挂起期间,该线程可能会被调用中断方法,线程在 park
期间,无法响应中断,所以只有当线程被唤醒,执行到第 3 行才会去检查 park
期间是否被调用过中断,如果有的话,则将该值传递出去,通过外层来响应中断。
通过对 acquireQueued
这个方法的分析,我们可以这么说,如果当前线程所在的节点处于头节点的后一个,那么它将会不断去尝试拿锁,直到获取成功。否则进行判断,是否需要挂起。这样就能保证 head 之后的一个节点在自旋 CAS 获取锁,其他线程都已经被挂起或正在被挂起。这样就能最大限度地避免无用的自旋消耗 CPU。
既然大量线程被挂起,那么就会有被唤醒的时机。上面也提到,当持有锁的线程释放了锁,那么将会尝试唤醒后续节点。下面看一下 release
方法。
public final boolean release(int arg) {
if (tryRelease(arg)) { // 尝试释放同步状态
Node h = head; // 获取头节点
if (h != null && h.waitStatus != 0) // 如果头节点存在且状态不为0
unparkSuccessor(h); // 唤醒后继节点
return true;
}
return false;
}
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
和 tryAcquire
一样,tryRelease
也是 AQS 开放给上层自由实现的抽象方法。
在 release
中,假如尝试释放锁成功,下一步就要唤醒等待队列里的其他节点,这里主要来看 unparkSuccessor
这个方法。参数是 head Node。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus; // 获取当前节点的等待状态
if (ws < 0) // 如果状态为 SIGNAL 或 PROPAGATE
compareAndSetWaitStatus(node, ws, 0); // 清除状态
Node s = node.next; // 获取当前节点的后继节点
if (s == null || s.waitStatus > 0) { // 如果后继节点为空或已取消
s = null;
for (Node t = tail; t != null && t != node; t = t.prev) // 从尾节点向前遍历
if (t.waitStatus <= 0) // 找到第一个未取消的节点
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); // 唤醒后继节点的线程
}
获取 head 的 waitStatus,如果不为 0,那么将其置为 0,表示锁已释放。接下来获取后续节点如果后续节点为 null 或者处于 CANCELLED 状态,那么从后往前搜索,找到除了 head 外最靠前且非 CANCELLED 状态的 Node,对其进行唤醒,让它起来尝试拿锁。
这时,拿锁、挂起、释放、唤醒都能够有条不紊,且高效地进行。
关于 9-11 行,可能有人疑惑,为什么不直接从头开始搜索,而是要花这么大力气从后往前搜索?
其实是和 addWaiter
方法中,前后两个节点建立连接的顺序有关。我们看:
- 后节点的
pre
指向前节点 - 前节点的
next
才会指向后节点
这两步操作在多线程环境下并不是原子的,也就是说,如果唤醒是从前往后搜索,那么可能前节点的 next
还未建立好,那么搜索可能会中断。