AQS源码分析总结(1)
@author:Jingdai
@date:2021.07.20
最近研究了一下AQS源码,记录一下,水平有限,不免理解有错,欢迎讨论指正。
整体思路
AQS是一个提供了简化同步类设计的框架,利用AQS可以比较容易的实现同步和互斥等功能。
AQS主要就是利用一个同步状态 state 来表示目前的同步状态,AQS负责管理这个同步状态。当线程无法得到同步资源时,需要将线程加入同步队列中,所以AQS 还负责管理一个同步队列。加入同步队列的同时,也涉及到线程的阻塞和唤醒,所以 AQS 还负责线程的阻塞和唤醒。AQS是一个抽象类,无法直接使用,子类必须定义更改同步状态的 protected 方法,并定义这个同步状态在获取或释放此对象方面的含义。换句话说,同步状态的具体含义是子类定义的。实现AQS的子类应定义为非公共内部帮助类。
整体来看,就是当一个线程想要获取锁,它需要先修改 state 的状态,如果可以修改,就直接利用CAS进行修改,如果现在锁被其他现在占用,则当前线程就进入同步队列中等待。当一个获得锁的线程运行完成后,它会去同步队列中唤醒队首节点。
AQS支持独占模式和共享模式中的一种或两种。 当以独占模式工作时,其他线程尝试获取不会成功。多个线程获取的共享模式可能(但不一定)成功。当共享模式获取成功时,下一个等待线程(如果存在)也必须确定它是否也可以获取。在不同模式下等待的线程共享同一个 FIFO 队列。通常,实现子类只支持这些模式中的一种,但也可能支持两种模式,如 ReadWriteLock 中。仅支持独占或共享模式的子类不需要定义未使用模式的方法。
相关的内部类
Node 源码
static final class Node {
// 标记这个节点是共享模式
static final Node SHARED = new Node();
// 标记这个节点是独占模式
static final Node EXCLUSIVE = null;
// waitStatus值:标记这个线程已经被取消
// 由于超时或中断,该节点被取消。
// 节点永远不会离开这个状态。
// 取消节点的线程永远不会再次阻塞。
// a thread with cancelled node never again blocks
static final int CANCELLED = 1;
// waitStatus值:标记这个节点的下一个节点的线程需要unparking
// 当这个节点 release 或 cancel 时需要unpark下一个节点
// 为了避免竞争,acquire必须首先表名它们需要signal,然后重试
// 原子获取,在失败时阻塞
static final int SIGNAL = -1;
// waitStatus值:标准这个节点在条件队列中
// 在传输前它不会用作同步队列节点,这个status
// 在传输时将会被设置为0
static final int CONDITION = -2;
// waitStatus值:下一个acquireShared应该无条件传播,很少用
// releaseShared 应该传播到其他节点。
// 这在 doReleaseShared 中设置(仅适用于头节点)
// 以确保传播继续,即使其他操作已经介入。
static final int PROPAGATE = -3;
// 0: None of the above
// 非负值意味着节点不需要发出信号。 因此,大多数代码不需要检查特定值,只需检查符号。
// 对于普通同步节点,该字段被初始化为 0,对于条件节点被初始化为 CONDITION。
volatile int waitStatus;
// 节点前驱(同步队列)
volatile Node prev;
// 节点后继(同步队列)
volatile Node next;
// 当前节点对应的线程
volatile Thread thread;
// 链接到下一个等待条件的节点,或特殊值 SHARED。
// 因为条件队列只有在独占模式下才会被访问,
// 所以当它们正在等待条件时,我们只需要一个简单的链接队列来保存节点
// 然后它们将被转移到队列以 re-acquire。
// 用在条件队列中,条件队列的连接不用prev和next。
// 条件队列是单向队列
Node nextWaiter;
// 共享模式没有条件队列,所以设置nextWaiter=SHARED表示共享模式
// 独占模式有条件队列,所以nextWaiter为节点。
final boolean isShared() {
return nextWaiter == SHARED;
}
}
AQS 类
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
// 等待队列的头部,延迟初始化。
// 除初始化外,仅通过 setHead 方法进行修改。
private transient volatile Node head;
// 同步队列的队尾,延迟初始化。
// 仅通过方法 enq 方法修改以添加新的等待节点
private transient volatile Node tail;
// 同步状态,最重要的属性
private volatile int state;
}
仅仅列出 AQS 重要的属性。
工作流程源码分析
为了减少篇幅,工作流程这里仅仅介绍独占模式,共享模式差不太多,理解了独占模式看共享模式也很容易。
acquire流程:
acquire方法
public final void acquire(int arg) {
// 尝试去获得锁,如果成功直接返回
if (!tryAcquire(arg) &&
// 获取不成功入队
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire是独占模式的获取资源方法。
tryAcquire
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
可以看出这个方法直接抛出异常,所以子类如果需要实现独占模式的语意的话,需要自定义这个方法的含义。这样实现的好处是如果子类不用这个方法的话就不用实现,子类只需要实现自己需要的方法,减轻开发者的压力。
addWaiter方法
private Node addWaiter(Node mode) {
// 此时mode为 Node.EXCLUSIVE
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
// 尝试一次CAS改变同步队列的队尾,如果成功,直接返回
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 失败,则用enq方法入队
enq(node);
return node;
}
这个方法就是将当前线程加入到同步队列的队尾。
enq方法
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
// 同步队列在加入一个节点前必须有一个头节点
// 头节点没有实际意义
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
// 通过CAS改变tail值
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued 方法
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 如果node是第二个节点,则还有一次机会去获得同步状态
if (p == head && tryAcquire(arg)) {
// 获得同步状态后会将head指向null
// 同时断开node和p的连接
// 再将 node 的 thread 设为null
setHead(node);
p.next = null; // help GC
failed = false;
// 返回是否中断过
return interrupted;
}
// 判断是否应该park,并检查中断状态
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
下面看 shouldParkAfterFailedAcquire 和 parkAndCheckInterrupt 方法。
shouldParkAfterFailedAcquire 方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 前驱节点已经设为SIGNAL,可以park了
// 之后 parkAndCheckInterrupt 方法就会park
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
// ws > 0 只有1,1代表取消
// 前驱被取消,就跳过前驱
// 然后进行下一轮重试(外面的函数中)
do {
// 先算后面的等号
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// waitStatus 是 0 or PROPAGATE
// 通过CAS修改,不一定能成功
// 也会进行下一轮重试
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt 方法
private final boolean parkAndCheckInterrupt() {
// 阻塞线程
LockSupport.park(this);
// 返回线程是否被中断
return Thread.interrupted();
}
结合这3个方法一起,看一下 acquireQueued 方法具体做了什么:
步骤1:得到node的前驱,如果前驱是head,尝试获取锁,成功则设置自己为头结点并断开和原来头结点的连接,函数返回。
步骤2:如果前驱不是head,则判断是否可以park(park的前提是前驱的waitStatus为-1)。如果其前驱的waitStatus 为-1,则阻塞并检测中断状态,等待被唤醒,唤醒后会回到步骤1。如果前驱的 waitStatus 是1,则一直跳过其前驱直到前驱的 waitStatus 小于等于 0,然后返回步骤1。如果前驱的 waitStatus 是-2,则尝试改为-1,之后返回步骤1。
再反过头来看这个 acquire 方法就很简单了。
acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
*acquire流程总结(重要):
步骤1:尝试获取锁,如果获取成功则直接返回。如果获取失败进入步骤2。
步骤2:调用 addWaiter 方法利用CAS进行入队,方法中尝试一次直接加入队尾,如果一次加入不成功就调用 enq 方法利用CAS加自旋入队。方法会返回入队的 node。进入步骤3。
步骤3:调用 acquireQueued 方法,一进入方法,设置 failed 和 interrupted 的初始值后,进入步骤4死循环。
步骤4:判断node的前驱,如果前驱是head,进入步骤5;如果前驱不是head,进入步骤6。
步骤5:尝试获取锁,成功则设置自己为头结点并断开和原来头结点的连接,函数返回,整个过程结束。也就是说如果入队的是第一个节点,则它还有一次获取锁的机会。进入步骤6。
步骤6:判断前驱的waitStatus,如果waitStatus值为-1,则阻塞并检测中断状态,等待被唤醒,唤醒后会回到步骤4。如果waitStatus值为1,则将node跳过前驱移动到前驱之前的位置上,重复这个动作直到前驱的waitStatus值不为1,然后回到步骤4。如果waitStatus值为-2,则尝试修改为-1,不管成功与否,都返回步骤4。
最后,如果步骤1没有获取到锁,后面返回的结果会有整个过程是否被中断过,如果中断过,会调用selfInterrupt 方法来设置本线程的中断状态。如下:
private static void selfInterrupt() {
Thread.currentThread().interrupt();
}
例子:
这里定义0是无锁,1是有锁,独占模式。
假如有A、B、C三个线程。
A 首先调用 acquire 方法,因为现在没有线程占有锁,所以A直接获得锁并返回。
B 调用 acquire 方法,首先调用 tryAcquire 方法尝试获得锁,因为 A 占用着锁,所以获取失败,然后会调用 addWaiter 方法进入同步队列,由于之前没有线程初始化同步队列,B就先去初始化同步队列,先创建一个头节点,然后将自己插入到同步队列中。接着就是调用 acquireQueued 方法,由于 B 前面就是head节点,所以 B 还会在去 tryAcquire 一次获取锁,由于 A 没有释放锁,所以还是失败。之后就将 B 的前驱改为 -1 ,然后调用 park阻塞,等待被中断或唤醒。
C 调用 acquire 方法,同样首先会调用 tryAcquire 方法尝试获得锁,因为 A 占用着锁,所以获取失败,然后会调用 addWaiter 方法进入同步队列,由于 B 已经初始化过同步队列,所以线程C不需要去初始化同步队列。直接创建一个自己的节点,将 waitStatus 设置为0,插入到队列中。然后调用 acquireQueued 方法,由于它前面是 B 节点,不是头节点,所以不会再去尝试获取锁,而是将自己的前驱节点的 waitStatus 设置为 - 1,然后挂起线程。
release 流程:
release 要比 acquire 简单许多,下面看具体的代码。
release方法
public final boolean release(int arg) {
// 尝试释放成功
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 唤醒 h 下一个节点
unparkSuccessor(h);
return true;
}
return false;
}
由于是独占模式,所以如果是获得锁的线程去 tryRelease 一定是可以成功的。
tryRelease方法
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
和 tryAcquire 方法一样,也需要子类去实现。
unparkSuccessor 方法
private void unparkSuccessor(Node node) {
// 如果node的的waitStatus 小于0
// 将 node 的waitStatus 改为0
int ws = node.waitStatus;
if (ws < 0)
// 为什么会有失败的情况???
compareAndSetWaitStatus(node, ws, 0);
// 如果node的next被取消了或者为null,
// 则从后往前找到第一个需要唤醒的node
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)
// 唤醒node的线程,被唤醒的线程将会从
// parkAndCheckInterrupt 方法处接着执行
// 会去尝试获得锁,获得锁后 acquire 就返回,
// 否则会再次park
LockSupport.unpark(s.thread);
}
同样总结一下release方法的整个过程。
*release整个过程(重要):
这个过程相对acquire来说简单许多,调用 tryRelease 尝试 release,如果成功则去唤醒等待队列的下一个元素,失败则直接返回false。
例子:
还是上面的例子,当 A 任务做完后,准备释放锁。
A 首先调用 tryRelease 方法,修改 state 的值为0,然后去唤醒同步队列中的元素。首先修改头节点的 waitStatus 值,改为0,然后叫起头结点的下一个元素,即B。
B 被唤醒之后,从 parkAndCheckInterrupt 方法中返回,又回到 acquireQueued 方法中,由于它的前驱就是head元素,所以会调用tryAcquire 方法获得锁,此时没有线程占有锁,所以可以获取到,然后将 head 指向自己,自己的线程设为null,并断开和之前 head 的连接(帮助GC),然后就返回。即 B 的 acquire 返回,接着就可以做后面的事情了。
其他细节部分
- AQS的同步队列比普通的队列多了一个头结点,这个头节点不记录线程信息,只有辅助作用,它在第一个node入队列的时候初始化。
- AQS虽然使用FIFO的队列,但并不能保证公平性,从上面可以看出 acquire 执行时,总会先去调用 tryAcquire 方法,而不管同步队列中是否有元素。所以当一个线程刚刚释放锁,同步队列的第一个等待节点还没有获得锁时,新来的线程就可能先获得锁,即插队。
参考
- Java8 API