这个接口为我们提供了2类方法,await()和signal(),其实现类ConditionObject,是AQS中的一个子类。在介绍AQS结构的文章中,ConditionObject类被跳过了,这个类的存在与CLH模型关联度不是很强,但在并发编程中却是不可或缺的一环,它提供的await()和signal()方法,能够为多线程之间交互提供帮助,能让线程暂停和恢复,是很重要的方法。
我们先来看一下它的内部结构。
成员变量:看来ConditionObject中也维护着一个队列,我们称它为“等待队列”。
private transient Node firstWaiter; // 首节点
private transient Node lastWaiter; // 尾结点
常量:
/** Mode meaning to reinterrupt on exit from wait */
private static final int REINTERRUPT = 1; // 从等待状态切换为中断状态
/** Mode meaning to throw InterruptedException on exit from wait */
private static final int THROW_IE = -1; // 抛出异常标识
实例方法很多,我们从最重要的开始分析,因为实现了Condition接口,因此await()和signal()就是切入点。
我先整理出一个await()内部调用流程:
// 向队列中添加节点并返回
private Node addConditionWaiter() {...}
// 释放节点持有的锁
final int fullyRelease(Node node) {...}
// 判断节点是否在同步队列中
final boolean isOnSyncQueue(Node node) {...}
// 检查线程是否中断,如果是则终止Condition状态并加入到同步队列
private int checkInterruptWhileWaiting(Node node) {...}
// 操作节点去申请锁
final boolean acquireQueued(final Node node, int arg) { ...}
// 清理等待队列中无效节点
private void unlinkCancelledWaiters() {...}
// 处理线程中断情况
private void reportInterruptAfterWait(int interruptMode){...}
接下来挨个分析实现方法,先从入口await()方法开始。
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter(); // t1
int savedState = fullyRelease(node); // t2
int interruptMode = 0;
while (!isOnSyncQueue(node)) { // t3
LockSupport.park(this); //t4
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) //t5
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE) // t6
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters(); // t7
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode); // t8
}
在t1位置,先看一下addConditionWaiter()方法,看名字是增加了一个条件等待对象,应该就是向等待队列中操作了。
private Node addConditionWaiter() {
Node t = lastWaiter; // 获取尾部指针,看来是采用尾插法
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters(); // t7'
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION); // 创建一个新节点
if (t == null) // 尾结点为空,说明队列是空的
firstWaiter = node; // 初始化队列
else
t.nextWaiter = node; // 尾插
lastWaiter = node; // 调整尾指针指向
return node; // 返回新增节点对象
}
t7和t7',在await()和addConditionWaiter()方法中,都调用了unlinkCancelledWaiters(),先看一下它做了什么:
private void unlinkCancelledWaiters() {
Node t = firstWaiter; // 拿到头节点
Node trail = null;
while (t != null) { // 如果头节点不为空,队列不为空
Node next = t.nextWaiter; // 遍历等待队列
if (t.waitStatus != Node.CONDITION) { // 如果节点的状态不是CONDITION
t.nextWaiter = null; // 将节点移除队列
if (trail == null) // 首次遍历,进度为0
firstWaiter = next; // 头节点指向被移除节点的下一个节点
else
trail.nextWaiter = next; // 进度指向下一个节点,也是将修复被移除队列节点的影响,保证队列连续
if (next == null)
lastWaiter = trail; // 如果next为空,说明队列遍历完成,将尾指针指向进度节点
}
else // 如果节点的状态是CONDITION
trail = t; // 保存进度
t = next;
}
}
那么unlinkCancelledWaiters()方法就做了一件事,遍历等待队列,将非CONDITION状态到的节点移除。
重点:在等待队列中,我们发现获取节点的后继节点时,使用的是nextWaiter属性,而非next,这就是区别“等待队列”和“同步队列”的关键。
在addConditionWaiter()方法的t7'位置调用的目的:调用条件是t.waitStatus != Node.CONDITION,也就是同步队列尾结点状态不对,那么这时清理一次同步队列再插入新节点很有必要。
在await()方法的t7位置调用的目的:由于t7前面还有其他逻辑未介绍,这里我们稍后继续分析。(调用条件是node.nextWaiter != null)
说回addConditionWaiter()方法,它其实和addWaiter()方法功能差不多,向队列中添加节点,这里的队列是“等待队列”。接着分析await()方法。
int savedState = fullyRelease(node); // t2
在t2位置,调用fullyRelease(node),传入新添加的node节点,并返回一个状态:
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState(); // 获取AQS中的state值
if (release(savedState)) { /// 调用释放锁方法
failed = false;
return savedState; // 如果释放成功,返回state值
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
在fullyRelease()方法中,主要是调用了release()去释放锁。这里有个前提就是线程必须先持有锁,才能调用await()方法,进而release()释放锁。
那么就引出了await()方法暂停线程,会导致锁被释放的逻辑。
release()方法的实现,前文有提到,需要回顾的请戳《面试必考AQS-排它锁的申请与释放》。我们继续分析await()方法。
while (!isOnSyncQueue(node)) { // t3
LockSupport.park(this); //t4
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) //t5
break;
}
在t3位置,调用了while循环,条件是!isOnSyncQueue(node),是否不在同步队列中? 如果不在,将会执行下面的内容。
final boolean isOnSyncQueue(Node node) {
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false; // 以节点状态作为判断条件,如果等于CONDITION(说明在等待队列中)、或者前置节点为空,是一个独立节点
if (node.next != null) // If has successor, it must be on queue
return true; // 如果后继节点不为空,说明它还在同步队列中。
/*
* node.prev can be non-null, but not yet on queue because
* the CAS to place it on queue can fail. So we have to
* traverse from tail to make sure it actually made it. It
* will always be near the tail in calls to this method, and
* unless the CAS failed (which is unlikely), it will be
* there, so we hardly ever traverse much.
* 前置节点为空,并不代表节点不在队列上,因为 CAS操作有可能失败。 因此需要从尾部遍历队列来保证它不在队列上。
*/
return findNodeFromTail(node); // 从尾部找到node节点
}
这里要注意一点,node的next属性是AQS的同步队列范畴的属性,在ConditionObject中是没有使用next属性的。这点在分析unlinkCancelledWaiters()方法时说明过。
// 这个方法没什么好解释的
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
那么,如果while (!isOnSyncQueue(node)) 成立,就是节点node不在同步队列上,则说明node已经释放锁了,并且进入了等待队列。接下来让线程挂起、等待被唤醒就可以了。
LockSupport.park(this); //t4
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) //t5
break;
在t5位置,执行的条件是线程被唤醒,唤醒后首先要检查的是,在这期间线程是否有被中断,保证线程安全。
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : // t5-1
0;
}
final boolean transferAfterCancelledWait(Node node) {
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) { // 将节点状态由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.
* 如果忘记调用signal,那么就不能继续执行了,要让它回到同步队列中。
*/
while (!isOnSyncQueue(node)) // 判断线程是否在同步队列,直到回到同步队列(取消,也要先让node回到同步队列)
Thread.yield(); // 让出CPU时间
return false; // 修改node状态失败,返回false
}
在t5-1位置,如果线程为中断状态,则进入transferAfterCancelledWait() ,里面会操作node状态由CONDITION回到初始状态0,此时如果操作成功,会将node重新放回同步队列。
如果CAS失败,则需要向下执行,有可能是其他操作改变了node状态,或许是取消的场景,因为这里进入的前提是线程已经被中断。
在结束了transferAfterCancelledWait()方法后,根据返回的true/false,确定 返回THROW_IE还是REINTERRUPT状态,如果没有中断则返回0,也就是interruptMode的初始值。
总结t5的逻辑,线程被唤醒后,检查线程状态,如果是中断状态,要尝试将node的节点状态变更为0,如果变更成功,则判定中断原因是异常,如果变更失败,要给线程时间让其他线程将node放回同步队列。
在t5位置,如果返回的不是初始值,则外层while会被break;如果是初始值,则会判断是否进入同步队列,是则结束循环,否则说明还在等待队列,需要继续被挂起。
当循环结束,后续流程就需要 让线程重新进入锁竞争状态,并且前面判断了那么多线程状态,也要根据返回值处理一下。
if (acquireQueued(node, savedState) && interruptMode != THROW_IE) // t6
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters(); // t7
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode); // t8
在t6位置,让节点线程再次去申请锁,同时传入挂起前保存的资源值saveState,节点回到竞争状态后就是AQS申请逻辑,可以交给AQS了;对于await()来说,剩下的就是处理线程状态了。
如果interruptMode != 异常,则调整interruptMode的值为REINTERRUPT。也就是说,如果线程申请锁成功,未来会让线程中断。
在t7位置,如果节点node有后继节点,那么需要将node从等待队列移除
在t8位置,如果interruptMode的值不为0,也就是不正常状态,进入reportInterruptAfterWait()方法。
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE) // 如果为异常状态
throw new InterruptedException(); // 抛出异常
else if (interruptMode == REINTERRUPT) // 如果为中断状态
selfInterrupt(); // 设置线程中断
}
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
以上就是await()方法的全部流程,大致可归纳为:
1、将持有锁的线程包装为node,并放入等待队列
2、将持有的锁释放,保持持有锁时申请的资源值
3、循环判断节点node释放在同步队列中,如果没有则挂起线程
4、线程被唤醒后,要判断线程状态
5、让线程去申请锁,根据申请规则,如果申请失败会在同步队列挂起
6、如果申请成功,要根据线程状态对线程进行合理的处理:抛异常或中断
先整理出一个signal()内部调用流程:
public final void signal() {...} // 唤醒线程入口
protected boolean isHeldExclusively(); // 判定当前线程是否持有锁
private void doSignal(Node first) {...} // 唤醒first节点
final boolean transferForSignal(Node node) {...} // 转换节点状态
从入口方法signnal()来分析:
public final void signal() {
if (!isHeldExclusively()) // 抽象方法,有子类实现,用于判断当前线程是否持有锁
throw new IllegalMonitorStateException(); // 只有持有锁的线程才能操作唤醒
Node first = firstWaiter; // 获取等待队列的头结点
if (first != null)
doSignal(first); // 执行唤醒操作
}
入口就是一些状态判断,真正执行唤醒的是doSignal()方法:
private void doSignal(Node first) {
do {
// t1
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) && // t2
(first = firstWaiter) != null); // t3
}
方法进入后,遇到一个do..while循环,先执行do内逻辑。
在t1位置,判断给定的节点first是否存在后继节点,如果不存在,将lastWaiter置为null。这里就是将等待队列清空。
接着进入t2位置,调用transferForSignal()方法:
final boolean transferForSignal(Node node) {
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) // 将节点状态恢复为0,如果修改失败返回false
return false; // t2-1
/*
* 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).
*/
Node p = enq(node); // 将恢复状态的节点,加入同步队列
int ws = p.waitStatus; // 获取加入节点的同步状态
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) // t2-2,或者,无法调整为SIGNAL
LockSupport.unpark(node.thread); // 唤醒线程
return true;
}
在t2-1位置,将节点状态恢复为0,如果修改失败返回false。
在t2-2位置,或的判断,两个条件:
1、如果同步状态为取消,则唤醒线程,在await()逻辑中,被唤醒的线程会检查线程状态,此时的取消会导致在transferAfterCancelledWait()方法中,无法将node状态由CONDITION转为0,也就进而不停让出线程cpu时间,导致线程被取消。
2、如果compareAndSetWaitStatus(p, ws, Node.SIGNAL)==false,也就是CA无法改变ws值,就说明有其他线程在操作该node。
以上两种条件都必须要唤醒线程。
while (!transferForSignal(first) && // t2
(first = firstWaiter) != null); // t3
当然以上两种条件有可能都不成立,那么就继续在t2位置执行循环,直到条件成立。
当执行了t2-1位置,也就代表节点node状态被重置,并且已经从等待队列出队,那么,在t3位置==遍历等待队列下一个节点。
在while条件中,完整逻辑是:不断尝试唤醒等待队列的头节点,直到找到一个没有被cancel的节点,跳出循环。
以上就是signal()方法的所有源码,归纳一下:
1、只有持有锁的线程才能操作唤醒
2、唤醒时要针对 等待队列 的头节点所代表的线程
3、唤醒= 线程节点node 状态重置 + node回到同步队列 + unpark线程
4、唤醒过程中如果遇到cancel状态的节点,要尝试等待队列中下一个,直到找到可被正常唤醒节点 或者 队列为空
在Condition接口中,还有一个signalAll()方法,目的是唤醒所有等待的节点,来分析一下源码:
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}
入口方法看了与signal()差不多,只是最后执行的方法是doSignalAll()。
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
与doSignal()的区别是 while流程有变化,它不是找到一个可被唤醒的节点就结束,而是遍历整个等待队列,将所有节点唤醒。
Condition及ConditionObject,实现了线程的等待与唤醒行为,在并发编程中,熟练使用它们能够大大提升并行效率,减少线程空转,降低CPU消耗。
自此,AQS的主要源码已经分析完毕,后面会挑选JUC下的主要实现类做分析,来看一下之前反复提到的tryxxx()方法是如何实现以达到不同特色、不同类型的锁的。
推荐阅读:
面试必考AQS-AQS概览
面试必考AQS-AQS源码全局分析
面试必考AQS-排它锁的申请与释放
面试必考AQS-共享锁申请、释放及传播状态
面试必考AQS-await和signal的实现原理