Java传家宝:微信公众号(Java传家宝)、Java传家宝-CSND
Condition
条件队列,在ReentrantLock里面继承了AQS的内部Condition的实现类ConditionObject,之前在AQS之ReentrantLock中由于篇幅问题没有继续讲解。先看一下之前画的AQS结构:
可以看到,每个线程节点都记录了nextWaiter属性,在我们之前加锁解锁分析没有见到他,其实他是用于条件队列的,用于形成单向链表结构。我们先看一下Condition结构的源码,其实没什么,就是定义了一些必须实现的方法而已:
public interface Condition {
// 等待
void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
// 唤醒
void signal();
void signalAll();
}
在看在AQS中的实现ConditionObject定义了那些属性:
public class ConditionObject implements Condition, java.io.Serializable {
private transient Node firstWaiter; // 条件队列队头
private transient Node lastWaiter; // 条件队列队尾
}
可以看到,记录了队头队尾而已,至于Node还是之前的定义。可以看一下条件队列的结构:
await & signal
在条件队列中,比较重要的就是await和signal方法了,即等待和唤醒,我们先看await的源码:
// AQS
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)) { // 判断是否在同步队列中
LockSupport.park(this); // 不在就挂起
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
addConditionWaiter
首先调用await方法之后,我们需要将当前线程加入到条件队列当中,通过addConditionWaiter,如下:
// AQS
private Node addConditionWaiter() {
Node t = lastWaiter; // 拿到条件队列的尾节点
// 如果尾节点不为null && waitStatus不是CONDITION
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters(); // 清理掉取消等待的条件队列节点
t = lastWaiter; // 拿到最新的尾节点
}
// 将当前线程包装为条件队列节点 初始化waitStatus为CONDITION
Node node = new Node(Thread.currentThread(), Node.CONDITION);
// 入队操作 随便看看
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
// 清理掉取消等待的条件队列节点
private void unlinkCancelledWaiters() {
Node t = firstWaiter; // 拿到条件队列处理节点 从前往后遍历 从首节点开始
Node trail = null; // 最终记录的是当前处理的节点的上一个节点,用于链表操作清理处理节点
while (t != null) { // 处理节点不为空
Node next = t.nextWaiter; // 处理节点的下一个节点
if (t.waitStatus != Node.CONDITION) { // 处理节点节点取消了等待
// 断掉连接 清理当前处理节点
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t; // 否则 记录当前处理节点
t = next; // 将下一个节点作为处理节点 继续上述操作
}
}
可以看到,条件队列的入队操作,首先是拿到队列的尾节点,判断是否为null&&当前尾节点是否取消了等待;是则通过unlinkCancelledWaiters方法将条件队列中的所有取消等待的节点从前往后遍历,全部清理掉。最后将当前线程包装为条件队列的节点,等待状态默认为CONDITION,然后入队即可。
fullyRelease
然后我们回到await方法,如下:
public final void await() throws InterruptedException {
if (Thread.interrupted()) // 检查中断状态
throw new InterruptedException();
Node node = addConditionWaiter(); // 将线程加入条件队列
int savedState = fullyRelease(node); // 完全释放锁 并保存锁状态 即重入次数
//...
}
可以看到,将当前线程入队之后,会释放完全释放锁资源(包括重入),让别的线程抢占。看一下fullyRelease里面的逻辑:
final int fullyRelease(Node node) {
boolean failed = true; // 失败标志默认为true
try {
int savedState = getState(); // 拿到锁状态
if (release(savedState)) { // 释放锁
failed = false; // 成功更新失败标志位false
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed) // 释放锁失败会将当前节点的waitStatus设置位取消获取锁
node.waitStatus = Node.CANCELLED;
}
}
释放锁的过程呢在AQS之ReentrantLock有说到,这里就不在说了。
isOnSyncQueue
然后我们回到await方法,如下:
// AQS
public final void await() throws InterruptedException {
//...
while (!isOnSyncQueue(node)) { // 判断是否在同步队列中
LockSupport.park(this); // 不在就挂起
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// ...
}
拿到锁状态且释放掉锁资源后,通过isOnSyncQueue判断当前线程节点是否在阻塞队列中,看一下里面的逻辑:
final boolean isOnSyncQueue(Node node) {
// 等待状态为CONDITION || 前驱节点为空 返回false
// 一般进入同步队列会更改状态为0
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
// 后继节点不为空 说明已经完全进入同步队列了 返回true
// 因为进入同步队列最后的操作就是设置后继节点
if (node.next != null)
return true;
// 后继节点为空 && 前驱节点不为空 && 等待状态不为CONDITION
return findNodeFromTail(node);
}
// 后继节点为空 && 前驱节点不为空 && 等待状态不为CONDITION
// 说明是未完全入队,设置了前驱节点,CAS操作不清楚,后继节点还未设置
// 从后往前遍历同步队列 直到找到对应的节点
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node) // 找到了返回true
return true;
if (t == null) // 找不到返回false
return false;
t = t.prev;
}
}
可以看到,这一部分就是判断当前线常节点是否已经进入同步队列了。具体判断过程与节点进入同步队列的过程有关,大概就是先通过等待状态和前驱节点判断,在通过后继节点判断,还不能确定就通过从后往前遍历查找。最终判断如果不在同步队列就挂起当前线程。
signal
唤醒线程。通过刚刚await操作,如果是第一次调用,一般不在同步队列中,而是在条件队列当中,所以会将当前线程挂起。也就是停留在如下代码上:
public final void await() throws InterruptedException {
//...
while (!isOnSyncQueue(node)) {
LockSupport.park(this); // 线程挂起
}
// ...
}
这个时候,只有等待其他线程唤醒才能够继续工作,也就是signal方法。看一下实现:
public final void signal() {
if (!isHeldExclusively()) // 当前线程持有锁才能唤醒
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null) // 同步队列不为空执行 为空还唤醒啥?
doSignal(first); // 从first开始唤醒
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null) // 记录first的后继节点(条件队列)
lastWaiter = null; // 后继节点为空 设置尾节点为null
// 从条件队列中移除first
// 因为要进入同步队列了或者first的等待状态不是CONDITION。顺便也清理了
first.nextWaiter = null;
} while (!transferForSignal(first) && // 从前往后找到满足条件的node
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))// 替换状态为0
return false; // 替换失败返回 说明当前节点已经不是CONDITION状态了 直接返回找下一个节点
// CAS成功执行下面代码
Node p = enq(node); // 将当前条件队列节点放入同步队列 返回的是前继节点
int ws = p.waitStatus;
// 前继节点取消等待||CAS替换状态为-1成功
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread); // 唤醒当前节点对应的线程
return true;
}
可以看到当线程经过进入条件队列,在到被挂起,在到经过signal的操作之后,条件队列的线程节点就加入到同步队列的队尾了。
signal之后
这个时候我们在回到await方法,线程被唤醒后,又干了啥:
// AQS
public final void await() throws InterruptedException {
// ...
while (!isOnSyncQueue(node)) { // 判断是否在同步队列中
LockSupport.park(this); // 不在就挂起
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//...
}
上一步 signal 之后,我们的线程由条件队列转移到了同步队列,之后就准备获取锁了。只要重新获取到锁了以后,继续往下执行checkInterruptWhileWaiting,看看干了啥:
private static final int REINTERRUPT = 1; // 表示重新中断
private static final int THROW_IE = -1; // 表示抛出中断异常
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ? // 是否中断?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
//
final boolean transferAfterCancelledWait(Node node) {
// CAS替换当前线程节点的等待状态为0
// 这块成功只能是signal之前发生中断了
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
enq(node); // 成功加入同步队列 这里可以发现 1中断会加入同步队列 2signal也会加入同步队列
return true;
}
// 失败进入循环
// 这里是指signal方法已经将状态改变了,但是还没有完成将其加入到同步队列中的操作就被中断了
while (!isOnSyncQueue(node)) // 是否在同步队列中
Thread.yield(); // 不在等待其完成
return false;
}
可以看到,checkInterruptWhileWaiting这个方法就是判断中断状态的,可以分为三个状态:
- 0: 代表整个过程中一直没有中断发生。
- 1 THROW_IE: 表示退出await()方法时需要抛出nterruptedException,这种模式对应于中断发生在signal之前
- -1 REINTERRUPT: 表示退出await()方法时只需要再自我中断一下,这种模式对应于中断发生在signal之后。
退出while循环
现在我们在回到await里面,观察这个循环体:
// AQS
public final void await() throws InterruptedException {
// ...
while (!isOnSyncQueue(node)) { // 判断是否在同步队列中
LockSupport.park(this); // 不在就挂起
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//...
}
经过刚刚的分析,要退出这个循环的话,要么当前线程节点已经在同步队列中了,要么当前线程处于中断状态。现在假设退出循环了,看一下后面的逻辑:
public final void await() throws InterruptedException {
//...
// 获取锁 && 中断状态不为THROW_IE
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // 不为null说明是在中断被加入到同步队列的
unlinkCancelledWaiters(); // 将条件队列中取消等待的清理掉
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
// 中断状态不为0
private void reportInterruptAfterWait(int interruptMode) throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException(); // 抛出异常
else if (interruptMode == REINTERRUPT)
selfInterrupt(); // 自我中断
}
在这一过程中我们尤其要关注中断,如前面所说,中断和signal所起到的作用都是将线程从条件队列中移除,加入到同步队列中去争锁,所不同的是,signal方法被认为是正常唤醒线程,中断方法被认为是非正常唤醒线程,如果中断发生在signal之前,则我们在最终返回时,应当抛出InterruptedException;如果中断发生在signal之后,我们就认为线程本身已经被正常唤醒了,这个中断来的太晚了,我们直接忽略它,并在await 返回时再自我中断一下,这种做法相当于将中断推迟至await0 返回时再发生。