Java并发学习笔记 (八) Condition
目录
一. Condition 简介
等待通知机制,在Java中主要有两种方式:
- 一是基于wait/notify方法结合synchronized关键字来实现
- 二是Condition结合Lock来实现
Condition的示例:
Condition接口定义了等待/通知两种类型的方法,在线程调用这些方法时,需要提前获取Condition对象关联的锁(在基于wait/notify方法实现的方案中需要获取的是对象锁)。
Condition对象是需要关联Lock对象的,调用Lock对象的newCondition()对象创建而来,也就是说Condition的使用是需要依赖Lock对象的。
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void conditionAwait() throws InterruptedException {
lock.lock();
try {
condition.await();
} finally {
lock.unlock();
}
}
public void conditionSignal() {
lock.lock();
try {
condition.signal();//condition.signalAll();
} finally {
lock.unlock();
}
}
说明:
await()方法会使当前线程等待,同时释放当前锁,当其他线程中使用signal()时或者signalAll()方法时,线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待。这和Object.wait()方法很相似。
awaitUninterruptibly()方法与await()方法基本相同,但awaitUninterruptibly()方法不会在等待过程中响应中断。
singal()方法用于唤醒一个在等待中的线程。相对的singalAll()方法会唤醒所有在等待中的线程。这和Obejct.notify()方法类似。
condition.await()方法必须在lock.lock()与lock.unlock()方法之间调用。
Condition.await: 让当前线程一直等到直到获取信号,或者发生中断.通过和
lock
锁互相配合,会使当前线程一直睡眠直到一下四种情况的其中一种发生:
另外一个线程调用当前
Condition
的signal
方法,这当前线程会被挑选出来,并被唤醒其他线程调用了当前
Condition
的signalAll
方法其他线程调用了当前线程的中断,这当前中断会被挂起
当前线程被假唤醒
Condition.awaitUninterruptibly
跟Condition.await
类似,只是不能被中断
Condition.await(long time, TimeUnit unit)
当前线程会被唤醒,要么是获得信号,要么是中断,要么是指定时间到
Condition.awaitUntil(Date deadline)
当前线程会被唤醒,要么是获得信号,要么是中断,要么是到了指定结束时间
Condition.signal
唤醒等待的线程.如果当前的condition
有多个线程在等待的话,这会唤醒其中一,且这个线程在返回await
前必须重新获得锁Condition.signalAll 唤醒所有等待的线程.如果当前的
condition
有多个线程在等待,则所有的线程都会被唤醒,且这些被唤醒的线程必须在返回await
之前重新获得锁
从整体上来看Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制,而Condition与Lock配合完成等待通知机制,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。两者的功能特上的区别在于:
- Condition能够支持不响应中断,而通过使用Object方式不支持;
- Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个;
- Condition能够支持超时时间的设置,而Object不支持
二. Condition实现原理
2.1 等待队列
创建一个condition对象是通过lock.newCondition()
,而这个方法实际上是会new出一个ConditionObject对象,该类是AQS的一个内部类,有兴趣可以去看看。前面我们说过,condition是要和lock配合使用的也就是condition和Lock是绑定在一起的,而lock的实现原理又依赖于AQS(参看AbstractQueuedSynchronizer(AQS)),自然而然ConditionObject作为AQS的一个内部类无可厚非。我们知道在锁机制的实现上,AQS内部维护了一个同步队列,如果是独占式锁的话,所有获取锁失败的线程的尾插入到同步队列,同样的,condition内部也是使用同样的方式,内部维护了一个 等待队列,所有调用condition.await方法的线程会加入到等待队列中,并且线程状态转换为等待状态。
如果认真学习理解了AQS的实现原理(参看AbstractQueuedSynchronizer(AQS)),那么对于下面Conditon实现原理的理解应该是水到渠成的。
Node类有这样一个属性:
//后继节点
Node nextWaiter;
等待队列是一个单向队列,等待队列是一个不带头结点的链式队列,而从AQS中可知同步队列是一个双向队列。示意图如下:
我们可以多次调用lock.newCondition()方法创建多个condition对象,也就是一个lock可以持有多个等待队列。而在之前利用Object的方式实际上是指在对象Object对象监视器上只能拥有一个同步队列和一个等待队列,而并发包中的Lock拥有一个同步队列和多个等待队列。示意图如下:
如图所示,ConditionObject是AQS的内部类,因此每个ConditonObject能够访问到AQS提供的方法,相当于每个Conditon都拥有所属同步器的引用。
2.2 await实现原理
当调用condition.await()方法后会使得当前获取lock的线程进入到等待队列,如果该线程能够从await()方法返回的话一定是该线程获取了与condition相关联的lock。await的源码如下:
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 1. 将当前线程包装成Node,尾插入到等待队列中
Node node = addConditionWaiter();
// 2. 释放当前线程所占用的lock,在释放的过程中会唤醒同步队列中的下一个节点
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
// 3. 当前线程进入到等待状态
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 4. 自旋等待获取到同步状态(即获取到lock)
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
// 5. 处理被中断的情况
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
当前线程调用condition.await()方法后,会使得当前线程释放lock然后加入到等待队列中,直至被signal/signalAll后会使得当前线程从等待队列中移至到同步队列中去,直到获得了lock后才会从await方法返回,或者在等待时被中断会做中断处理。
在第1步中调用addConditionWaiter将当前线程添加到等待队列中,该方法源码为:
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
//将当前线程包装成Node
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
//尾插入
t.nextWaiter = node;
//更新lastWaiter
lastWaiter = node;
return node;
}
这段代码将当前节点包装成Node,如果等待队列的firstWaiter为null的话(等待队列为空队列),则将firstWaiter指向当前的Node,否则,更新lastWaiter(尾节点)即可。就是通过尾插入的方式将当前线程封装的Node插入到等待队列中即可,同时可以看出等待队列是一个不带头结点的链式队列,之前我们学习AQS时知道同步队列是一个带头结点的链式队列,这是两者的一个区别。将当前节点插入到等待对列之后,会使当前线程释放lock,由fullyRelease方法实现,fullyRelease源码为:
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;
}
}
这段代码表示,调用AQS的模板方法release方法释放AQS的同步状态并且唤醒在同步队列中头结点的后继节点引用的线程,如果释放成功则正常返回,若失败的话就抛出异常。
那么,如何退出await方法呢?
while (!isOnSyncQueue(node)) {
// 3. 当前线程进入到等待状态
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
很显然,当线程第一次调用condition.await()方法时,会进入到这个while()循环中,然后通过LockSupport.park(this)方法使得当前线程进入等待状态,那么要想退出这个await方法第一个前提条件自然而然的是要先退出这个while循环,出口就只剩下两个地方:1. 逻辑走到break退出while循环;2. while循环中的逻辑判断为false。再看代码出现第1种情况的条件是当前等待的线程被中断后代码会走到break退出,第二种情况是当前节点被移动到了同步队列中(即另外线程调用的condition的signal或者signalAll方法),while中逻辑判断为false后结束while循环。总结下,就是当前线程被中断或者调用condition.signal/condition.signalAll方法当前节点移动到了同步队列后 ,这是当前线程退出await方法的前提条件。当退出while循环后就会调用acquireQueued(node, savedState)
,这个方法在介绍AQS的底层实现时说过了,若感兴趣的话可以去看这篇文章,该方法的作用是在自旋过程中线程不断尝试获取同步状态,直至成功(线程获取到lock)。这样也说明了退出await方法必须是已经获得了condition引用(关联)的lock。
调用condition.await方法的线程必须是已经获得了lock,也就是当前线程是同步队列中的头结点。调用该方法后会使得当前线程所封装的Node尾插入到等待队列中。
2.3 signal / signalAll实现原理
调用condition的signal或者signalAll方法可以将等待队列中等待时间最长的节点移动到同步队列中,使得该节点能够有机会获得lock。按照等待队列是先进先出(FIFO)的,所以等待队列的头节点必然会是等待时间最长的节点,也就是每次调用condition的signal方法是将头节点移动到同步队列中。
public final void signal() {
//1. 先检测当前线程是否已经获取lock
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//2. 获取等待队列中第一个节点,之后的操作都是针对这个节点
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
signal方法首先会检测当前线程是否已经获取lock,如果没有获取lock会直接抛出异常,如果获取的话再得到等待队列的头指针引用的节点,之后的操作的doSignal方法也是基于该节点。
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
//1. 将头结点从等待队列中移除
first.nextWaiter = null;
//2. while中transferForSignal方法对头结点做真正的处理
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
//1. 将头结点从等待队列中移除
first.nextWaiter = null;
//2. while中transferForSignal方法对头结点做真正的处理
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
//1. 更新状态为0
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* 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).
*/
//2.将该节点移入到同步队列中去
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
调用condition的signal的前提条件是当前线程已经获取了lock,该方法会使得等待队列中的头节点即等待时间最长的那个节点移入到同步队列,而移入到同步队列后才有机会使得等待线程被唤醒,即从await方法中的LockSupport.park(this)方法中返回,从而才有机会使得调用await方法的线程成功退出。
sigllAll与sigal方法的区别体现在doSignalAll方法上,前面我们已经知道doSignal方法只会对等待队列的头节点进行操作,,而doSignalAll的源码为:
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
该方法只不过时间等待队列中的每一个节点都移入到同步队列中,即“通知”当前调用condition.await()方法的每一个线程。