前言:
在了解 Condition 的源码之前,你需要对 AQS 的源码有一定的认识,因为 Condition 的功能是建立在 AQS 的基础之上,我的上篇文章对 AQS 进行了源码分析。
Condition 的基本使用
Condition是一个多线程协调通信的工具类,可以让某些线程一起等待某个条件(condition),只有满足条件时,线程才会被唤醒。
- 案例演示
当调用 await方法后,当前线程会释放锁并等待,而其他线程调用 condition 对象的 signal 或者 signalall 方法通知并被阻塞的线程,然后自己执行unlock释放锁,被唤醒的线程获得之前的锁继续执行,最后释放锁。
所以,condition 中两个最重要的方法,一个是 await,一个是signal方法
- await:把当前线程阻塞挂起
- signal:唤醒阻塞的线程
可以发现 Condition 和我们的 synchronized 的wait/notify 方法很像,所以我们可以了解到 Condition 这个类就是 J.U.C 用来实现线程的主动阻塞和唤醒功能。
Condition 源码分析
- 看一下 ConditionObject 的结构,只有两个属性,组成了一个单向链表
- firstWaiter 指向队列中的头结点
- lastWaiter 指向队列中的尾节点
- 这里有个很重要的点,ConditionObject是 AQS 的内部类,所以它自动有着外部类的引用,因为他是根据 ReentrantLock 对象中的 sync 创建的,而 sync 继承了 AQS 类,所以他和 ReentrantLock 对象共享一个 AQS 队列。
首先调用Condition,需要获得Lock锁,所以意味着会存在一个AQS同步队列,在上面那个案例中,两个线程同时运行,那么AQS的队列可能是下面这种情况,至于为什么请看我的上篇文章,后续已经讲过的细节我会直接跳过。
然后调用了 condition.await() 方法
- awai() 源码分析
调用Condition的await()方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了 Condition 相关联的锁
public final void await() throws InterruptedException {
//表示 await 允许被中断
if (Thread.interrupted())
throw new InterruptedException();
//创建一个新的节点,节点状态为 condition,采用的数据结构仍然是链表
Node node = addConditionWaiter();
//释放当前的锁,得到锁的状态,并唤醒 AQS 队列中的一个线程
long savedState = fullyRelease(node);
//如果当前节点没有在同步队列上,即还没有被 signal,则将当前线程阻塞
int interruptMode = 0;
//判断这个节点是否在 AQS 队列上,第一次判断的是 false,因为前面已经释放锁了
while (!isOnSyncQueue(node)) {
//阻塞线程
LockSupport.park(this);
//当线程醒来后,会判断他到底是被 single唤醒的,还是通过中断操作被唤醒的
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
//如果是通过single唤醒的,结束循环,如果通过中断操作唤醒的,则继续阻塞,添加进 AQS 队列
break;
}
// 当这个线程醒来,会尝试拿锁, 当 acquireQueued 返回 false 就是拿到锁了.
// interruptMode != THROW_IE -> 表示这个线程没有成功将 node 入队,但 signal 执行了 enq 方法让其入队了.
// 将这个变量设置成 REINTERRUPT.
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//如果 node 的下一个等待者不是 null, 则进行清理,清理 Condition 队列上的节点.
//如果是 null ,就没有什么好清理的了.
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
//如果线程被中断了,需要抛出异常.或者什么都不做
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
- 第一个重要的方法 addConditionWaiter()这个方法的主要作用是把当前线程封装成 Node,添加到 Condition 中的等待队列。这里的队列不再是双向链表,而是单向链表。
private Node addConditionWaiter() {
//获取 lastWaiter指向的 Node 节点
Node t = lastWaiter;
//如果lastWaiter不等于空 && 节点的状态不等于 CONDITION 时,把lastWaiter指向的节点删除,然后重新获取 lastWaiter 指向的节点
if (t != null && t.waitStatus != Node.CONDITION) {
//删除 lastWaiter 指向的节点,并且更新 lastWaiter 指向的节点
unlinkCancelledWaiters();
t = lastWaiter;
}
//为当前线程创建一个 Node 节点,并将它的状态设置为 CONDITION(-2)
Node node = new Node(Thread.currentThread(), Node.CONDITION);
//如果 lastWaiter 指向的Node的节点为空,firstWaiter指向当前线程的节点
if (t == null)
firstWaiter = node;
//如果不为空 更新 lastWaiter 指向当前线程的 Node 节点
else
t.nextWaiter = node;
//更新 lastWaiter 指向的节点
lastWaiter = node;
//返回当前线程的节点
return node;
}
执行完addConditionWaiter这个方法之后,就会产生一个这样的condition队列
- fullRelease,就是彻底的释放锁,什么叫彻底呢,就是如果当前锁存在多次重入,那么在这个方法中只需要释放一次就会把所有的重入次数归零。
final long fullyRelease(Node node) {
//定义 failed 标识
boolean failed = true;
try {
//获得重入的次数
long savedState = getState();
//释放锁并且唤醒下一个同步队列中处于阻塞状态的线程
if (release(savedState)) {
//修改 failed 标识
failed = false;
//返回重入的次数
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
//如果唤醒线程失败,则修改当前线程节点的状态为 CANCELLED(1)
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
- 此时,同步队列会触发锁的释放和重新竞争。ThreadB 获得了锁。
- isOnSyncQueue() 这个方法比较麻烦
final boolean isOnSyncQueue(Node node) {
//判断当前线程的节点状态是否为 CONDITION || 他的前一个节点是否null
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
//判断他的下一个节点
if (node.next != null)
return true;
//如果在 AQS 队列中找到了当前线程的节点返回true,否则返回false
return findNodeFromTail(node);
}
判断当前节点是否在同步队列中,返回false表示不在,返回true表示在
如果不在AQS同步队列,说明当前节点没有唤醒去争抢同步锁,所以需要把当前线程阻塞起来,直到其他的线程调用signal唤醒。
如果在AQS同步队列,意味着它需要去竞争同步锁去获得执行程序执行权限
为什么要做这个判断呢?原因是在 condition 队列中的节点会重新加入到AQS队列去竞争锁。也就是当调用 signal 的时候,会把当前节点从 condition 队列转移到 AQS 队列
判断 ThreadA 这个节点是否存在于AQS队列中
- 如果ThreadA的waitStatus的状态为CONDITION,说明它存在于 condition 队列中,不在 AQS 队列。因为 AQS 队列的状态一定不可能有CONDITION。
- 如果node.prev为空,说明也不存在于AQS队列,原因是 prev=null 在 AQS 队列中只有一种可能性,就是它是 head 节点,head节点意味着它是获得锁的节点。
- 如果 node.next 不等于空,说明一定存在于 AQS 队列中,因为只有AQS队列才会存在next和prev的关系。
- findNodeFromTail,表示从tail节点往前扫描AQS队列,一旦发现 AQS 队列的节点和当前节点相等,说明节点一定存在于AQS队列中。
- 此时 threadA 已经被阻塞了,并且 threadB 已经拿到锁了,接着执行 signal() 方法
public final void signal() {
//先判断当前线程是否获得了锁,这个判断比较简单,直接用获得锁的线程和当前线程相比即可
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//拿到 firstWaiter 指向的节点
Node first = firstWaiter;
if (first != null)
//如果不为空,则把 first节点代表的线程加入到 AQS 队列中
doSignal(first);
}
- .doSignal() 方法:对condition队列中从首部开始的第一个condition状态的节点,执行transferForSignal操作,将node从condition 队列中转换到 AQS 队列中,同时修改 AQS 队列中原先尾节点的状态。
private void doSignal(Node first) {
do {
//从 condition 队列中删除 first 节点,并且判断 condition 队列是否为空队列
if ( (firstWaiter = first.nextWaiter) == null)
// 将 lastWaiter 节点设置成 null
lastWaiter = null;
//将 first 的 nextWaiter 属性设置为null
first.nextWaiter = null;
//添加进AQS队列
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
- transferForSignal()方法,该方法先是 CAS 修改了节点状态,如果成功,就将这个节 点放到 AQS 队列中,然后唤醒这个节点上的线程。此时,那个节点就会在 await 方法中苏醒
final boolean transferForSignal(Node node) {
//更新节点的状态为 0,如果更新失败,只有一种可能就是节点被 CANCELLED 了
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//调用 enq() 方法把当前节点添加进 AQS 队列中
Node p = enq(node);
//拿到当前线程节点的状态
int ws = p.waitStatus;
//如果上一个节点的状态被取消了, 或者尝试设置上一个节点的状态为 SIGNAL 失败了(SIGNAL表示: 他的 next 节点需要停止阻塞)
//如果成功添加进 AQS 队列,则返回true,取反等于false,结束循环
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
//唤醒线程
LockSupport.unpark(node.thread);
return true;
}
- 图解分析
执行完doSignal以后,会把condition队列中的节点转移到aqs队列上,逻辑结构图如下这个时候会判断ThreadA的prev节点也就是head节点的waitStatus,如果大于0或者设置SIGNAL失败,表示节点被设置成了CANCELLED状态。这个时候会唤醒 ThreadA 这个线程。否则就基于AQS队列的机制来唤醒,也就是等到ThreadB释放锁之后来唤醒ThreadA。
- checkInterruptWhileWaiting() 方法,前面在分析await方法时,线程会被阻塞。而通过signal被唤醒之后又继续回到上次执行的代码 checkInterruptWhileWaiting 这个方法是干嘛呢?就是判断ThreadA在condition队列被塞的过程中,有没有被其他线程触发过中断请求
private int checkInterruptWhileWaiting(Node node) {
//判断在唤醒线程前是否调用了中断方法
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
-
如果当前线程被中断,则调用 transferAfterCancelledWait 方法判断后续的处理应该是 抛出InterruptedException还是重新中断。
-
transferAfterCancelledWait() 方法
final boolean transferAfterCancelledWait(Node node) {
//使用 cas 修改节点状态,如果还能修改成功,说明线程被中断时,signal 还没有被调用。
// 这里有一个知识点,就是线程被唤醒,并不一定是在 java 层面执行了 locksupport.unpark,也可能是调用了线程的 interrupt()方法,这个方法会更新一个中断标识,并且会唤醒处于阻塞状态下的线程。
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
enq(node);
//如果 cas 成功,则把 node 添加到 AQS 队列
return true;
}
//如果 cas失败,则判断当前node是否已经在AQS队列上,如果不在,则让给其他线程执行
//当node被触发了signal方法时,node就会被加到AQS队列上
while (!isOnSyncQueue(node))
//循环检测 node 是否已经成功添加到 AQS 队列中。如果没有,则通过yield主动释放CPU时间片,
Thread.yield();
return false;
}
这里需要注意的地方是,如果第一次CAS失败了,则不能判断当前线程是先进行了中断还是先进行了signal方法的调用,可能是先执行了signal然后中断,也可能是先执行了中断,后执行了signal,当然,这两个操作肯定是发生在CAS之前。这时需要做的就是等待当前线程的node被添加到AQS队列后,也就是enq方法返回后,返回false告诉checkInterruptWhileWaiting方法返回 REINTERRUPT(1),后续进行重新中断。
简单来说,该方法的返回值代表当前线程是否在park的时候被中断唤醒,如果为true表示中断在signal调用之前,signal还未执行,那么这个时候会根据await的语义,在await时遇到中断需要抛出 interruptedException,
返回true就是告诉 checkInterruptWhileWaiting返回THROW_IE(-1)。
如果返回false,否则表示signal已经执行过了,只需要重新响应中断即可
await 和 signal 的总结
我把前面的整个分解的图再通过一张整体的结构图来表 述,线程awaitThread先通过lock.lock()方法获取锁成功 后调用了condition.await方法进入等待队列,而另一个线程signalThread通过lock.lock()方法获取锁成功后调用了condition.signal或者signalAll方法,使得线程 awaitThread 能够有机会移入到同步队列中,当其他线程释放lock后使得线程awaitThread能够有机会获取 lock,从而使得线程awaitThread能够从await方法中退出执行后续操作。如果awaitThread获取lock失败会直接进入到同步队列。
- 阻塞:await()方法中,在线程释放锁资源之后,如果节点不在AQS等待队列,则阻塞当前线程,如果在等待队列,则自旋等待尝试获取锁
- 释放:signal()后,节点会从condition队列移动到 AQS 等待队列,然后进入正常锁的获取流程