查看
java.lang.Object#wait(long)
方法的注释时,发现了在注释中给了一个使用范例,JDK中其他地方竟然也使用了这种范例,于是来了兴趣,记录一下。
一、原注释
简要说明,就是为了防止 “ 伪唤醒 ”等,需要使用条件循环来做再次判断。
好的。这里提到了两本书有详细的介绍。找了这两本书的说明如下:
1.1 、《Java并发编程实战》
说明:
过早唤醒
虽然在锁、条件谓词和条件队列之间的三元关系并不复杂,但 wait 方法的放回并不一定意味着线程正在等待的条件谓词已经变成真了。
内置条件队列可以与多个条件谓词一起使用。当一个线程由于调用 notifyAll 而醒来时,并不意味该线程正在等待的条件谓词已经变成真了。(这就像烤面包机和咖啡机共用一个铃声,当响铃后,你必须查看是哪个设备发出的铃声。)另外,wait 方法还可以 “ 假装 ” 返回,而不是由于某个线程调用了 notify。
当执行控制重新进入调用 wait 的代码时,它已经重新获取了与条件队列相关联的锁。现在条件谓词是不是已经变为真了?或许。在发出通知的线程调用 notifyAll 时,条件谓词可能已经变成真,但在重新获取锁时将再次变为假。在线程被唤醒到 wait 重新获取锁的这段时间里,可能有其他线程已经获取了这个锁,并修改了对象的状态。或者,条件谓词从调用 wait 起根本就没有变成真。你并不知道另一个线程为什么调用 notify 或 notifyAll ,也许因为与同一条件队列相关的另一个条件谓词变成了真。“ 一个条件队列与多个条件谓词相关 ” 是一种很常见的情况。
基于所有这些原因,每次线程从 wait 中唤醒时,都必须再次测试条件谓词,如果条件谓词不为真,那么就继续等待(或者失败)。由于线程在条件谓词不为真的情况下也可以反复地醒来,因此必须在一个循环中调用 wait,并在每次迭代中都测试条件谓词。条件等待的标准形式如下:
void stateDependentMethod() throws InterruptedException{
// 必须通过一个锁来保护条件谓词
synchronized(lock){
while(!conditionPredicate())
lock.wait()
// 现在对象处于合适的状态
}
}
1.2、《Effective Java》
说明:
并发工具优先于 wait 和 notify
虽然你始终应该优先使用并发工具,而不是使用 wait 和 notify 方法,但可能必要维护使用了 wait 和 notify 方法的遗留代码。 wait 方法被用来是线程等待某个条件。它必须在同步区域内部被使用,这个同步区域将对象锁定在了调用 wait 方法的对象上。下面是使用 wait 方法的标准模式:
// The standard idiom for using the wait method
synchronized (obj){
while (<condition does not hold>)
obj.wait(); // (Releases lock, and reacquires on wakeup)
...// Perform action appropriate to condition
}
始终应该使用 wait 循环模式来调用 wait 方法;永远不要在循环之外调用 wait 方法。
循环会在等待之前和之后对条件经行测试。
在等待之前测试条件,当条件已经成立时就跳过等待,这对于确保活性是必要的。如果条件已经成立,并且在线程等待之前,notify (或者 notifyAll)方法已经被调用,则无法保证该线程会从等待中苏醒过来。
在等待之后测试,如果条件不成立的话继续等待,这对于确保安全性是必要的。当条件不成立的时候,如果线程继续执行,则可能会破坏被锁保护的约束条件。当条件不成立时,有下面一些理由可使一个线程苏醒过来:
- 另一个线程可能已经得到了锁,并且从一个线程调用 notify 方法那一刻起,到等待线程苏醒过来的这段是时间中,得到锁的线程已经改变了保护的状态。
- 条件并不成立,但是另一个线程可能意外地或恶意地调用了 notify 方法。在公有可访问的对象上等待,这些类实际上把自己暴露在了这种危险的境地中。公有可访问对象的同步方法中包含的 wait 方法都会出现这样的问题。
- 通知线程(notifying thread)在唤醒等待线程时可能会过度 “ 大方 ”。例如,即使只有某些等待线程的条件已经被满足,但是通知线程可能仍然调用 notifyAll 方法。
- 在没有通知的情况下,等待线程也可能(但很少)会苏醒过来。这被称为 “ 伪唤醒 ”(spurious wakeup)。
二、JDK中的使用范例
java.lang.Thread#join(long)
:
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
// 循环判断条件,进入等待
while (isAlive()) {
wait(0);
}
} else {
// 循环判断条件,进入等待
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
三、等待 / 通知的经典范式
该范式分为两部分,分别针对等待方(消费者)和通知方(生产者)。
等待方遵循如下原则:
- 获取对象的锁。
- 如果条件不满足,那么调用对象的 wait() 方法,被通知后仍要检查条件。
- 条件满足则执行对应的逻辑。
对应的伪代码如下:
synchronized(对象){
while(条件不满足){
对象.wait();
}
对应的处理逻辑
}
通知方遵循如下原则:
- 获取对象的锁。
- 改变条件。
- 通知所有等待在对象上的线程。
对应的伪代码如下:
synchronized(对象){
改变条件
对象.notifyAll();
}
参考
《Java并发编程的艺术》
《Java并发编程实战》
《Effective Java》