一、简介
1.1 什么是Condition
任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括 wait()、wait(long timeout)、notify() 以及 notifyAll() 方法,这些方法与 synchronized 同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。——摘自《Java并发编程的艺术》
下面是Condition与Object的监视器方法的对比(摘自《Java并发编程的艺术》):
1.2 Condition接口
我们来看一下Condition接口的定义:
public interface Condition {
//等待,当前线程接受到信号前、或中断前一直处于等待状态
void await() throws InterruptedException;
//等待,当前线程接受到信号前一直处于等待状态,不响应中断
void awaitUninterruptibly();
//等待,当前线程接受到信号前、或中断前、或到达指定等待时间之前一直处于等待状态,返回值 = nanosTimeout - 实际消耗的时间,返回值 <= 0表示超时
long awaitNanos(long nanosTimeout) throws InterruptedException;
//等待,当前线程接受到信号前、或中断前、或到达指定等待时间之前一直处于等待状态,返回boolean类型,表示是否在指定时间内获取到接受信号,false表示超时。
//它与上一个方法不同在于可以自定义超时时间单位,等同于awaitNanos(unit.toNanos(time)) > 0
boolean await(long time, TimeUnit unit) throws InterruptedException;
//等待,当前线程在接收到信号前、被中断或到达指定最后唤醒一个等待线程。如果所有的线程都在等待此条件,则选择其中的一个唤醒。在从 await 返回之前,该线程必须重新获取锁。期限之前一直处于等待状态
boolean awaitUntil(Date deadline) throws InterruptedException;
//唤醒一个等待线程。如果所有的线程都在等待此条件,则选择其中的一个唤醒。在从 await 返回之前,该线程必须重新获取与Condition相关的锁。
void signal();
//唤醒所有等待线程。如果所有的线程都在等待此条件,则唤醒所有线程。在从 await 返回之前,每个线程都必须重新获取与Condition相关的锁。
void signalAll();
}
Condition 是一种广义上的条件队列。他为线程提供了一种更为灵活的等待/通知模式,线程在调用 await() 方法后执行挂起操作,直到线程等待的某个条件为真时才会被唤醒。Condition 必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个 Condition 的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现。
二、基本使用
以基本的B线程唤醒A线程为例:
2.1 Object#wait/notify 等待唤醒写法
public static void main(String[] args) throws InterruptedException {
//对象锁
Object obj=new Object();
//创建线程A
Thread threadA = new Thread(()->{
System.out.println("A尝试获取锁...");
synchronized (obj){
System.out.println("A获取锁成功!");
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("A开始释放锁,并开始等待...");
//线程A开始等待
obj.wait();
System.out.println("A被通知继续运行,直至结束。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//创建线程B
Thread threadB = new Thread(()->{
System.out.println("B尝试获取锁...");
synchronized (obj){
System.out.println("B获取锁成功!");
try {
TimeUnit.SECONDS.sleep(3);
//线程B开始唤醒线程A
obj.notify();
System.out.println("B随机通知lock对象的等待队列中某个线程!");
System.out.println("B执行完毕,开始释放锁...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//启动线程A
threadA.start();
//这里为了是线程A先执行,避免B先执行了notify导致A永远无法被唤醒
TimeUnit.SECONDS.sleep(1);
//启动线程B
threadB.start();
}
//执行结果
A尝试获取锁...
A获取锁成功!
A开始释放锁,并开始等待...
B尝试获取锁...
B获取锁成功!
B随机通知lock对象的等待队列中某个线程!
B执行完毕,开始释放锁...
A被通知继续运行,直至结束。
2.2 Condition#await/signal 等待唤醒写法
public static void main(String[] args) throws InterruptedException {
//创建lock对象
Lock lock = new ReentrantLock();
//新建condition
Condition condition = lock.newCondition();
//创建线程A
Thread threadA = new Thread(()->{
System.out.println("A尝试获取锁...");
lock.lock();
try {
System.out.println("A获取锁成功!");
TimeUnit.SECONDS.sleep(1);
System.out.println("A开始释放锁,并开始等待...");
//线程A开始等待
condition.await();
System.out.println("A被通知继续运行...");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
System.out.println("A线程释放了锁,执行结束!");
}
});
//创建线程B
Thread threadB = new Thread(()->{
System.out.println("B尝试获取锁...");
lock.lock();
try {
System.out.println("B获取锁成功!");
TimeUnit.SECONDS.sleep(3);
//线程B开始唤醒线程A
condition.signal();
System.out.println("B随机通知lock对象的等待队列中某个线程!");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
System.out.println("B线程释放了锁,执行结束!");
}
});
//启动线程A
threadA.start();
//这里为了是线程A先执行,避免B先执行了notify导致A永远无法被唤醒
TimeUnit.SECONDS.sleep(1);
//启动线程B
threadB.start();
}
//执行结果
A尝试获取锁...
A获取锁成功!
A开始释放锁,并开始等待...
B尝试获取锁...
B获取锁成功!
B随机通知lock对象的等待队列中某个线程!
B线程释放了锁,执行结束!
A被通知继续运行...
A线程释放了锁,执行结束!
三、源码解析
3.1 ConditionObject类
ConditionObject 是 Condition 在java并发中的具体的实现,由于Condition的操作需要获取相关的锁,而AQS则是同步锁的实现基础,所以ConditionObject则定义为AQS的内部类。每个Condition对象都对应一个等待队列。ConditionObject定义如下:
// AbstractQueuedSynchronizer
public class ConditionObject implements Condition, java.io.Serializable {
//序列化版本号
private static final long serialVersionUID = 1173984872572414699L;
//条件(等待)队列的第一个节点
private transient Node firstWaiter;
//条件(等待)队列的最后一个节点
private transient Node lastWaiter;
//。。。。。。
}
3.1.1 等待队列
每个 ConditionObject 都包含一个FIFO队列,队列中的节点类型是AQS的内部类——Node类,每个节点包含着一个线程引用,该线程就是在该Condition对象上等待的线程。与CLH同步队列不同的是,Condition的等待队列是单向队列,即每个节点仅包含指向下一节点的引用,如下图所示:
3.1.2 ConditionObject#await() 方法解析
调用 Condition#await() 方法会使当前线程进入等待状态,同时会加入到Condition等待队列同时释放锁。await() 方法代码如下:
// AbstractQueuedSynchronizer#ConditionObject
public final void await() throws InterruptedException {
//响应中断
if (Thread.interrupted())
throw new InterruptedException();
//将当前线程加入到等待队列中---------(1)
Node node = addConditionWaiter();
//释放当前线程持有的锁锁资源,不管当前线程重入多少次,全部置0---------(2)
int savedState = fullyRelease(node);
//中断模式
int interruptMode = 0;
//判断节点是否在同步队列(SyncQueue)中,即是否被唤醒---------(3)
while (!isOnSyncQueue(node)) {
//如果不在同步队列,则在JVM级别挂起当前线程
LockSupport.park(this);
//线程被唤醒后,检查是否发生了中断---------(4)
// 被唤醒有两种原因:1是发生了中断,2是别的线程调用了signal
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break; //如果发现线程已经中断了,则直接退出循环
}
//被唤醒后执行自旋操作尝试获取锁,同时判断线程是否被中断
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
//获取锁成功,则标记线程需要重新设置中断标记位
interruptMode = REINTERRUPT;
//取消后进行清理
if (node.nextWaiter != null) // clean up if cancelled
//清理等待队列中不为CONDITION状态的节点
unlinkCancelledWaiters();---------(5)
if (interruptMode != 0)
//根据中断模式处理中断---------(6)
reportInterruptAfterWait(interruptMode);
}
从如上代码观察中,不难发现,await() 主要做了四件事:
- 调用 addConditionWaiter() 方法构建新的节点封装线程信息并将其加入等待队列。
- 调用 fullyRelease(node) 释放锁资源(不管此时持有锁的线程重入多少次都一律将state置0),同时唤醒同步队列中后继节点的线程。
- 调用 isOnSyncQueue(node) 判断节点是否存在同步队列中,在这里是一个自旋操作,如果同步队列中不存在当前节点则直接在JVM级别挂起当前线程。
- 当前节点线程被唤醒后,即节点从等待队列转入同步队列时,则调用 acquireQueued(node, savedState) 方法执行自旋操作尝试重新获取锁资源。
注释标记了6个重点方法,分别来看看:
(1)addConditionWaiter() 方法:将线程封装为节点,加入到等待队列
如果没有中断异常抛出,则调用 addConditionWaiter() 方法将当前线程加入到等待队列中,方法如下:
// AbstractQueuedSynchronizer#ConditionObject
private Node addConditionWaiter() {
//获取尾结点
Node t = lastWaiter;
//如果t不为空且其等待状态不为CONDITION,表示该节点不处于等待状态,需要清除掉
if (t != null && t.waitStatus != Node.CONDITION) {
//清理等待队列中不为CONDITION状态的节点
unlinkCancelledWaiters();
//重新获取新的尾结点
t = lastWaiter;
}
//创建包含当前线程的结点,状态为CONDITION
Node node = new Node(Thread.currentThread(), Node.CONDITION);
//如果t为空,则表示队列为空,因此新节点为第一个节点
if (t == null)
//设置头结点
firstWaiter = node;
//否则队列不为空,将节点插入到最后一个位置
else
//节点插入到最后一个位置
t.nextWaiter = node;
//设置尾结点
lastWaiter = node;
//返回节点
return node;
}
//清除条件队列中所有状态不为Condition的节点
private void unlinkCancelledWaiters() {
//获取头结点
Node t = firstWaiter;
Node trail = null;
//从头往尾清除掉等待状态不为CONDITION的节点
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;
}
}
(2)fullyRelease() 方法:完全释放锁
当前节点添加到等待队列成功后,会进行调用 fullyRelease() 方法完全释放锁。代码如下:
// AbstractQueuedSynchronizer#ConditionObject
final int fullyRelease(Node node) {
//是否失败标志
boolean failed = true;
try {
//获取同步状态
int savedState = getState();
//释放同步状态
if (release(savedState)) {
//失败标志为否
failed = false;
//返回原同步状态
return savedState;
}
//如果释放锁失败,直接抛出异常
else {
throw new IllegalMonitorStateException();
}
} finally {
//如果操作失败,则将节点状态置为无效
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
(3)isOnSyncQueue() 方法:检测此节点是否在同步队列中
如果不在,说明该线程还不具备竞争锁的资格,挂起线程继续等待,代码如下:
// AbstractQueuedSynchronizer#ConditionObject
public final void await() throws InterruptedException {
//......
//自旋检测此节点是否在同步队列中,如果不在,说明该线程还不具备竞争锁的资格,挂起线程继续等待
while (!isOnSyncQueue(node)) {
//挂起线程
LockSupport.park(this);
//线程被唤醒后,检查是否发生了中断
// 被唤醒有两种原因:1是发生了中断,2是别的线程调用了signal
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break; //如果发现线程已经中断了,则直接退出循环
}
//......
}
我们来看一下 isOnSyncQueue() 方法的实现:
// AbstractQueuedSynchronizer#ConditionObject
//检测此节点是否在同步队列
final boolean isOnSyncQueue(Node node) {
//若是当前节点状态为CONDITION 或者说node前驱节点为null,说明节点不在同步队列里
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
//节点状态不为CONDITION 且node前驱节点存在,并且后继节点存在,则认为在同步队列里
//这是为什么呢?因为调用该方法之前node节点插入的位置是等待队列的末尾,next域肯定为null,如果不为空,说明肯定不在等待队列中,即在CLH队列中
if (node.next != null) // If has successor, it must be on queue
return true;
//从尾往头查找CLH队列,看是否存在node节点
//后继节点不存在,可能是节点加入到同步队列尾部的时候,CAS修改tail指向不成功,因此此处再遍历同步队列直接比较节点是否相等
return findNodeFromTail(node);
}
//判断CLH同步队列否存在node节点
private boolean findNodeFromTail(Node node) {
//获取节点
Node t = tail;
//从后往前遍历查找是否存在node节点
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
(4)checkInterruptWhileWaiting() 方法:返回是否发生过中断以及中断的模式
// AbstractQueuedSynchronizer#ConditionObject
//返回是否发生过中断以及中断的模式: 如果在signal之前中断,则返回THROW_IE;如果在signal之后中断,则返回REINTERRUPT;如果没有中断,则为0。
private int checkInterruptWhileWaiting(Node node) {
//如果发生线程被中断,执行方法transferAfterCancelledWait,否则返回0
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
//transferAfterCancelledWait(xx)返回true,表示需要抛出中断异常,返回false,表示只需要将中断标记位补上就好了
final boolean transferAfterCancelledWait(Node node) {
//走到这,说明线程曾经被中断过,接下来就是要判断signal动作是否发生了
//尝试修改节点状态
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
//成功,则加入到同步队列里
//CAS 成功表明此时还没有发生signal()
enq(node);
return true;
}
//说明CAS失败,失败的原因是:signal()里已经将node状态改变了。
//因此,此处只需要等待signal()将节点加入到同步队列即可。
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
(5)unlinkCancelledWaiters() 方法:从等待队列里移除被取消的节点
虽然前面已经将节点加入到同步队列里,但是可能并没有将节点从等待队列里移除(没有调用signal的情况),因此这里需要检测一下。
// AbstractQueuedSynchronizer#ConditionObject
//清除条件队列中所有状态不为Condition的节点
private void unlinkCancelledWaiters() {
//获取头结点
Node t = firstWaiter;
Node trail = null;
//从头往尾清除掉等待状态不为CONDITION的节点
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;
}
}
(6)reportInterruptAfterWait() 方法:根据中断模式处理
中断模式 interruptMode == THROW_IE,表示需要抛出中断异常;interruptMode == REINTERRUPT,表示只需要将中断标记位补上。
// AbstractQueuedSynchronizer#ConditionObject
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
//直接抛出异常,发生了中断,但是还没有signal
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
//将中断标记位补上
selfInterrupt();
}
3.1.3 ConditionObject#signal() 方法解析
// AbstractQueuedSynchronizer#ConditionObject
public final void signal() {
// 判断当前线程是否持有独占锁资源,如果未持有则直接抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
// 唤醒等待队列第一个节点的线程
if (first != null)
doSignal(first);
}
在这里,singal() 唤醒方法一共做了两件事:
- 判断当前线程是否持有独占锁资源,如果调用唤醒方法的线程未持有锁资源则直接抛出异常(共享模式下没有等待队列,所以无法使用Condition)。
- 唤醒等待队列中的第一个节点的线程,即调用doSignal(first)方法。
我们来看看 doSignal(first) 方法的实现:
// AbstractQueuedSynchronizer#ConditionObject
private void doSignal(Node first) {
do {
// 移除等待队列中的第一个节点,如果nextWaiter为空
// 则代表着等待队列中不存在其他节点,那么将尾节点也置空
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
// 如果被通知上个唤醒的节点没有进入同步队列(可能出现被中断的情况),
// 等待队列中还存在其他节点则继续循环唤醒后继节点的线程
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
// transferForSignal()方法
final boolean transferForSignal(Node node) {
/*
* 尝试修改被唤醒节点的waitStatus为0即初始化状态
* 如果设置失败则代表着当前节点的状态不为CONDITION等待状态,
* 而是结束状态了则返回false返回doSignal()继续唤醒后继节点
* 为什么说设置失败则代表着节点不为CONDITION等待状态?
* 因为可以执行到此处的线程必定是持有独占锁资源的,
* 而此处使用的是cas机制修改waitStatus,失败的原因只有一种:
* 预期值waitStatus不等于CONDITION
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 快速追加到同步队列尾部,同时返回前驱节点p
Node p = enq(node);
// 判断前驱节点状态是否为结束状态或者在设置前驱节点状态为SIGNAL失败时,
// 唤醒被通知节点内的线程
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// 唤醒node节点内的线程
LockSupport.unpark(node.thread);
return true;
}
在如上代码中,可以通过我的注释发现,doSignal() 也只做了三件事:
- 将被唤醒的第一个节点从等待队列中移除,然后再维护等待队列中 firstWaiter 和 lastWaiter 的指向节点引用。
- 将等待队列中移除的节点追加到同步队列尾部,如果同步队列追加失败或者等待队列中还存在其他节点的话,则继续循环唤醒其他节点的线程。
- 加入同步队列成功后,如果前驱节点状态已经为结束状态或者在设置前驱节点状态为 SIGNAL 失败时,直接通过 LockSupport.unpark() 唤醒节点内的线程。
至此,signal() 方法逻辑结束,不过需要注意的是:我们在理解 Condition 的等待/唤醒原理的时候需要将 await()/signal() 方法结合起来理解。在 signal() 逻辑完成后,被唤醒的线程则会从前面的 await() 方法的自旋中退出,因为当前线程所在的节点已经被移入同步队列,所以 while (!isOnSyncQueue(node)) 条件不成立,循环自然则终止,进而被唤醒的线程会调用 acquireQueued() 开始尝试获取锁资源。