目录
简介
Condition 是一个接口,它提供了类似 Object 的监视器方法,如 wait(), notify() 等等,与 Lock 配合实现等待与通知的模式,但是这两种的使用方式以及它们的功能特性还是有差异的,下面通过对比两者来了解下这两个的特性(列表内容摘自 <<Java并发编程的艺术>>)
对比项 | Object Monitor Methods | Condition |
---|---|---|
前置条件 | 获取对象的锁 | 调用 Lock.lock() 获取锁 调用 Lock.newCondition() 获取Condition 对象 |
调用方式 | 直接调用 如:object.wait() | 直接调用 如: condition.await() |
等待队列个数 | 一个 | 多个 |
当前线程释放锁并进入等待状态 | 支持 | 支持 |
当前线程释放锁并进入等待状态,在 等待状态中不响应中断 | 不支持 | 支持 |
当前线程释放锁并进入超时等待状态 | 支持 | 支持 |
当前线程释放锁并进入等待状态到将 来的某个时间 | 不支持 | 支持 |
唤醒等待队列中的一个线程 | 支持 | 支持 |
唤醒等待队列中的全部线程 | 支持 | 支持 |
Condition 定义了一系列接口来实现等待和通知功能:
- void await() : 当前线程进入等待状态直到被通知和中断,当前线程才进入运行状态并且从await()方法中返回
- void awaitUninterruptibly() : 当前线程进入等待状态直到被通知,对中断不敏感
- long awaitNanos(long nanosTimeout) : 当前线程进入等待状态直到被通知,中断或超时
- boolean awaitUntil(Date deadline) : 当前线程进入等待状态直到被通知、中断或到某个时间,如果到了指定时间还没被通知,则返回false
- void signal : 唤醒一个等待在 Condition 上的线程
- void signalAll() : 唤醒所有等待在 Condition 上的线程
Condition 解析
1. Condition 对象的获取
首先,我们要如何获取 Condition 对象的呢? 上面有说到 Condition 跟 Lock 实现了等待与通知,那我们先从 Lock 中找找看
public interface Lock {
Condition newCondition();
// 其他接口省略
}
在 Lock 接口中有 newCondition() 这么一个方法,这个方法就是用来创建 Condition 对象的,但是这个是个抽象方法,那我们得找下实现这个接口的类,还记得之前讲过的 ReentrantLock 嘛, 它就是实现了 Lock 接口,那来看看它是怎么实现 newCondition方法的
public Condition newCondition() {
return sync.newCondition();
}
我们在 ReentrantLock 中找到了 newCondition, 它是调用 sync 的 newCondition 来获取的,点这个方法进去
final ConditionObject newCondition() {
return new ConditionObject();
}
这个方法是 ReentrantLock 的内部类 Sync 中的, 它创建了一个 ConditionObject 对象,那这个对象跟 Condition 有什么关系呢,我们接下来继续看
public class ConditionObject implements Condition, java.io.Serializable {
// 略....
}
原来 ConditionObject 实现了 Condition 接口,并且这个类是 AQS 的一个内部类。
2. Condition 的属性
知道怎么获取到 Condition后,接下来我们来看其功能是如何实现的。首先来看看 ConditionObject 有哪些属性
// 等待队列的首节点
private transient Node firstWaiter;
// 等待队列的尾节点
private transient Node lastWaiter;
等待队列也是一个 FIFO 的队列,其队列的节点也是使用 AQS 中的内部类 Node 来实现的,每个 Condition 实例都包含了一个等待队列,Condition 拥有首节点和尾节点。当前线程调用 Condition.await() 方法时,会将当前线程构造成节点,并将节点从尾部加入等待队列,等待队列的结构如下图:
如图所示, Condition 拥有队列的首尾节点的引用,如果要新增节点的话只需将尾节点的 nextWaiter 指向新节点, 然后更新尾节点就可以。由于要调用 await() 方法必须先获取到锁,所以这里不需要保证线程安全。
3. 等待
下面我们通过代码来了解等待是如何实现的,先进入 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)) {
// 线程等待
LockSupport.park(this);
// 线程中断退出
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 尝试获取同步状态
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 清除等待队列上状态不是 condition 的节点
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
上面代码的流程是先将当前线程构造成 Node 节点,并添加到等待队列中,然后释放掉同步状态,再通过 while 循环判断该节点是否在同步队列上,如果不是则进入等待状态,是则尝试去获取同步状态
添加到等待队列的源码如下:
private Node addConditionWaiter() {
// 获取尾节点
Node t = lastWaiter;
// 如果尾节点的等待状态不是 Condition, 则清掉等待队列中状态不是Condition的节点
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
// 构建节点
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
上面代码的流程是先获取尾节点,如果尾节点的等待状态不等于 Condition 的话,则调用 unlinkCanceledWaiters 方法从首节点开始遍历清除等待队列中等待状态不为 Condition 的节点, 然后将当前线程构建成 Node 节点,存进等待队列中。
释放锁的源码如下:
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;
}
}
代码中 release 方法中是调用了 AQS 子类重写的 tryRelease 方法释放同步状态,如果当前线程不是获取到独占锁的线程,则抛出异常。
判断是否在同步队列的源码:
final boolean isOnSyncQueue(Node node) {
// 节点的等待状态为 Condition 或者前驱节点为null时返回false
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
// 节点的后继节点不为null时说明节点在同步队列上
if (node.next != null) // If has successor, it must be on queue
return true;
// 从尾节点遍历同步队列 node 节点是否在同步队列上面
return findNodeFromTail(node);
}
4. 通知
通过调用 Condition 的 signal 方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。Condition 的 signal 的源码如下:
public final void signal() {
// 判断当前线程是不是拥有锁的线程
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 获取首节点,调用 doSignal 方法
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
先判断当前线程是否拥有锁,然后唤醒等待队列中的首节点
private void doSignal(Node first) {
do {
// 修改头节点,并将旧头节点移出等待队列
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
doSignal 方法是更新等待队列中的头节点,并将旧头节点移出等待队列,然后调用 transferForSignal 方法将旧头节点加入同步队列中去
final boolean transferForSignal(Node node) {
// 将节点的等待状态改成0,如果修改失败则说明当前节点已被取消
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 将节点加到同步状态中
Node p = enq(node);
int ws = p.waitStatus;
// 如果节点p的状态为cancel状态或者修改 waitStatus状态失败则直接唤醒
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
到这整个通知流程就结束了。
总结
一个线程要想调用 Condition 的 await 方法,首先要获取锁,调用 await 方法后会将该线程获取的同步状态释放掉,并将该线程构造成 Node 节点,加入到等待队列中,然后通过 while 循环调用 isOnSyncQueue 方法判断节点是否进入了同步队列,如果不是则进入等待状态。同样一个线程要想调用 Condition 的 signal 方法也必须先获取锁,然后将等待队列的头节点移出,其后继节点设置为头节点,然后将移出的节点加入到同步队列中,然后唤醒该线程,这样线程就可以从 await 方法返回,进入到争夺同步状态中去