AQS源码分析总结(2)
@author:Jingdai
@date:2021.07.20
上一篇介绍了 acquire 和 release 方法的详细流程,这两个方法都是对同步队列进行操作的,AQS 框架中还有一个重要的队列,就是条件队列,这篇文章将介绍 AQS 的条件队列及其相关的操作。
整体思路
当调用 AQS 的 await 方法时,AQS 就会把当前线程加入到对应的条件队列中去等待。等其他的线程调用 signal 方法时,就会把这个线程从条件队列中移出,放入同步队列中。
条件队列的设计思路和 Java 内置的 synchronize 的等待队列十分相似,学习的时候可以类比去学。比较大的差别是 AQS 可以有多个条件队列,而 synchronize 的等待队列只能有一个。注意一点,条件队列仅仅工作在独占模式,共享模式不存在条件队列。
相关的内部类
ConditionObject 源码
public class ConditionObject implements Condition, java.io.Serializable {
// 条件队列的第一个 node
private transient Node firstWaiter;
// 条件队列的最后一个 node
private transient Node lastWaiter;
// xx
}
上面仅仅列出了最重要的属性,同时注意条件队列和同步队列中的 Node 使用的ADT是一样的,但是同步队列是双向队列,使用 prev 和 next 前后链接,而条件队列是单向队列,使用 Node 的 nextWaiter 属性连接。
工作流程源码分析
await流程:
await 方法
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 创建一个新的节点并插入到条件队列队尾
Node node = addConditionWaiter();
// 释放锁并返回释放前的同步状态值
int savedState = fullyRelease(node);
int interruptMode = 0;
// 只要这个节点不在同步队列上,就阻塞
while (!isOnSyncQueue(node)) {
// 看后面的transferForSignal方法
// 会发现signal不一定会唤醒线程,只有当
// node 的前一个节点被取消或 node
// 的前一个节点waitStatus设置-1
// 失败才会唤醒线程,让线程走到下面
// acquireQueued 去重新同步
// 否则signal仅仅会把节点移到同步队列上,
// 等前一个节点唤醒自己时才会取消阻塞
// 这里自己的思考是,即使signal唤醒线程,如果
// 不是队列第一个节点,也就会走2步到acquireQueued
// 后会再次阻塞,没有必要,所以signal选择让
// 前一个节点唤醒,这样直接结束await的概率更大
LockSupport.park(this);
// 检测是否有中断:在signal前被中断返回THROW_IE ;
// 在signal后被中断返回 REINTERRUPT ;
// 没有中断返回0
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 节点已经到了同步队列上或者被中断了才会运行到这里,
// 被中断节点也会在同步队列上。
// 在同步队列中继续等待获得锁
// 获得的同步状态要和 await 阻塞
// 前一致,所以传入的 savedState
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 清理条件队列
// 后面讲 transferAfterCancelledWait
// 方法时会说明为什么会出现node.nextWaiter
// 不为null 的情况,就是中断时,将node从条件队列
// 转移到同步队列时,没有从条件队列中删除这个节点,
// 就会出现这个情况,这时需要清理
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
// 根据中断状态把处理中断补上
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
addConditionWaiter 方法
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
// 队尾节点不为null,且它的waitStatus不是 -2
// waitStatus 不等于-2,等于1时代表被取消
// transferAfterCancelledWait 方法中有一种条件
// 会使转移 node 节点时,node 不从条件队列中删除直接就
// 转移到同步队列中,这时 waitStatus = 0,也需要清除
if (t != null && t.waitStatus != Node.CONDITION) {
// 链表操作,从条件队列中删除所有的waitStatus 不等于
// -2的节点。仅仅在条件等待期间发生取消或插入新元素时尾
// 结点发现已被取消(这里就是)时才被调用。
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
从上面可以看出,条件队列和同步队列还有一点区别,条件队列没有无实际含义的头结点,当第一个节点插入时直接就是头节点,而同步队列第一个节点插入前需要先初始化一个无实际含义的头节点。
fullyRelease 方法
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
// 独占模式,如果是获得锁的线程
// 调用release方法,肯定可以成功
// 在release 方法中会释放锁,并唤醒
// 头结点后的节点(release不懂看一下上一篇)
if (release(savedState)) {
failed = false;
// 返回释放释放前的同步状态值
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
checkInterruptWhileWaiting 方法
private int checkInterruptWhileWaiting(Node node) {
// 线程没有被中断过,返回0
// 在signal前被中断返回THROW_IE ;
// 在signal后被中断返回 REINTERRUPT ;
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
这个 checkInterruptWhileWaiting 方法返回 wait 过程中中断的情况,除此之外,这个方法还有一个重要的任务,看 transferAfterCancelledWait 方法。
transferAfterCancelledWait 方法
final boolean transferAfterCancelledWait(Node node) {
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
enq(node);
return true;
}
/*
* If we lost out to a signal(), then we can't proceed
* until it finishes its enq(). Cancelling during an
* incomplete transfer is both rare and transient, so just
* spin.
*/
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
如果 node 的 waitStatus 等于-2,代表它还在条件队列上,这时尝试把这个值改为0,并把这个节点移动到同步队列上,返回true,代表中断发生在 signal 之前。否则,说明中断发生在signal之后,这时需要让当前线程让出cpu,等待其它线程signal操作完成,即将node 转移到同步队列上后直接返回false。
也就是这个方法结束之后,node一定会在同步队列上。这样才能执行 acquireQueued 方法。否则由于中断跳出 await 方法的下面这个循环时就会导致node不在同步队列上,之后就无法执行 acquireQueued(node) 方法。
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
// 检测是否有中断:在signal前被中断返回THROW_IE ;
// 在signal后被中断返回 REINTERRUPT ;
// 没有中断返回0
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
同时还要注意一点,这个方法中如果第一步 CAS 成功将 node 的 waitStatus 设置为0,那么在将这个节点转移到同步队列时并没有将这个节点从等待队列中删除,即这个时候这个节点同时在同步队列和条件队列上(我猜这也是同步队列和条件队列不共同使用一个next的原因,同步队列使用next,条件队列使用nextWaiter),这个在条件队列中的节点应该被后续清理掉。
*await流程总结(重要):
步骤1:调用 addConditionWaiter 方法创建一个此线程的node,此node的 waitStatus 等于-2。然后查看条件队列是否已经初始化,如果已经初始化了,就把刚刚创建的 node 加到队列尾部;否则初始化条件队列,使 firstWaiter 和 lastWaiter 指向node。
步骤2:调用 fullyRelease 保存并释放当前线程占有的同步状态,即执行 release 操作。(在release操作中会去调用unparkSuccessor 方法唤醒同步队列中第一个需要唤醒的节点)
步骤3:然后进入循环并 park 自己,只要 node 不在同步队列上且没有被中断,就一直在循环中阻塞。await 的目的就达到了,在这里等待中断或被唤醒。当 node 被signal时,进入步骤4,当node被中断时,进入步骤5。
步骤4:当 node 被 signal 时,会把这个 node 转移到同步队列中,如果 node 的前一个节点已经取消(waitStatus值为1)或者设置前驱节点的waitStatus值为-1失败时,会唤醒线程,跳出循环。否则仅仅将这个节点转移到同步队列中,并将前一个节点的 waitStatus值设置为-1 就 signal 方法返回了,线程仍然在这里阻塞。(后面signal方法会详细介绍)
步骤5:当 node 在阻塞的过程中被中断了,会跳出循环。同时,会调用 checkInterruptWhileWaiting 方法去判断是否需要将 node 移动到同步队列上,如果中断时还没有 signal 说明需要,就转移 node 到同步队列上;否则等待signal 方法将 node 转移到同步队列上。
步骤4如果没有被唤醒就会等待同步队列的队首元素来唤醒并退出循环进入步骤6;步骤4如果已经唤醒就会直接进入步骤6;步骤5执行完也会进入步骤6。
步骤6:进入到这步时,不管是从4过来还是从5过来,node都已经在在同步队列上了,步骤6会调用 acquireQueued 尝试获取步骤2保存的同步状态,获取不到就阻塞。但是这里阻塞是在同步队列中,而步骤3的阻塞是在条件队列中。
步骤7:当acquireQueued 获取到锁之后,执行一些清理工作后再调用 reportInterruptAfterWait 处理一下中断状态后就退出了await方法。接着执行后面的代码。
ps:后面有几步部分细节await的方法中没有讲,在signal方法中会介绍,知道大概继续看,看完signal就会更加明白。
signal流程:
signal 方法
public final void signal() {
// 判断当前线程是否是持有同步状态的线程
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
signal 方法的流程看着很简单,首先判断调用 signal方法的线程是否持有同步状态,如果是的话就调用 doSignal 方法。
isHeldExclusively 方法
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
这个方法需要子类去实现,当调用这个方法的线程就是持有同步状态的线程时,就返回 true,否则返回 false。
doSignal 方法
private void doSignal(Node first) {
do {
// 给firstWaiter赋新值,
// 如果为null,lastWaiter也为null
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
// 断开 first 与后面的连接
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
此处要注意,调用 doSignal 方法的只可能有一个线程。方法从条件队列中删除第一个节点并把这个节点传输到同步队列上,直到传输成功或第一个节点为null才退出循环。
transferForSignal 方法
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
// 如果node被取消了,返回false
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
// enq 入队,返回的是node的前驱
Node p = enq(node);
int ws = p.waitStatus;
// ws > 0 代表这个节点已经取消,
// 这时没有设置前驱为-1唤醒自己
// 看await方法会发现unpark后调用
// acquireQueued 方法,这个方法中会
// 会调用 shouldParkAfterFailedAcquire
// 方法在检查时删除取消的节点,并把前驱设置为-1
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// ws > 0 唤醒(原因如上)
// ws <= 0 且 设置-1失败 唤醒
// 这两种情况是为了唤醒这个节点来重新同步,
// 在 acquireQueued 方法中会处理好
LockSupport.unpark(node.thread);
// * 尝试设置成功不会唤醒线程
return true;
}
这个方法将节点从条件队列转移到同步队列,如果成功则返回true。失败则返回false,失败的原因是因为在signal前这个节点已经被取消了,或者这个节点由于中断已经被传输到同步队列上,但是条件队列中没有删除,此时node 的 waitStatus 为 0。
signal 流程总结:
步骤1:首先判断当前线程是否是持有同步状态的线程,是的话就进入步骤2,不是则抛出IllegalMonitorStateException异常。
步骤2:调用 doSignal 方法,将条件队列中第一个没有取消的非 null 节点转移到同步队列的队尾,进入步骤3。 如果第一个节点为null,signal函数返回。
步骤3:转移完成后,如果转移的 node 的前驱的waitStatus 为1(已经取消)或者尝试修改 node 的前驱的 waitStatus 值为-1失败,则唤醒这个node,接着 node 的线程会在await方法中调用 acquireQueued 方法把前一个节点的状态修改好或跳过前一个节点,具体看 acquireQueued 的流程(上一篇)。然后signal完成。
从上面的流程可以看出,signal并不一定会唤醒线程,只有在转移节点node后,node的前驱节点已经取消(waitStatus值为1)或者设置前驱节点的waitStatus值为-1失败时,才会唤醒线程,否则仅仅将这个节点转移到同步队列中,并将前一个节点的 waitStatus值设置为-1 就返回了,需要等到同步队列中前一个节点唤醒 node 时才被唤醒。
public final void await() throws InterruptedException {
// xx
while (!isOnSyncQueue(node)) {
LockSupport.park(this); //1
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 2
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// xx
}
如上 await 代码,这里自己的思考是,由于 signal 是将node转移到同步队列的队尾,即使将节点唤醒,也仅仅是让节点的线程从1处唤醒,到了2由于在队尾,在 acquireQueued 这里又要阻塞,要等到移动到队首才能被前一个节点唤醒,所以不如一直阻塞等前一个节点一下唤醒,直接到2很可能就不会在 acquireQueued 里阻塞。因为 park 的调用的代价应该也是很大的。(没有研究过,但是线程切换的开销肯定比较大),所以作者设置为等前一个节点唤醒自己时再取消阻塞是很有道理的。自己的一点理解,不免有错,欢迎讨论指正。
参考
- Java8 API