为什么wait()总是在一个循环中被调用
我读过,我们应该总是从一个循环内调用wait()
:
while (!condition) { obj.wait(); }
它工作正常,没有一个循环,为什么呢?
你不仅需要循环,而且还要检查循环中的条件。 Java不保证你的线程只会被notify()/ notifyAll()调用或者notify()/ notifyAll()调用所唤醒。 由于此属性,无环版本可能会在您的开发环境中工作,并在生产环境中意外失败。
例如,你正在等待的东西:
synchronized (theObjectYouAreWaitingOn) { while (!carryOn) { theObjectYouAreWaitingOn.wait(); } }
一个邪恶的线索出现,并且:
theObjectYouAreWaitingOn.notifyAll();
如果这个邪恶的线程不能/不能把这个carryOn
弄糟,你只要继续等待正确的客户端。
编辑:增加了更多的样本。 等待可以中断。 它会抛出InterruptedException,你可能需要在等待try-catch。 根据您的业务需求,您可以退出或抑制exception并继续等待。
它在Object.wait(long milis)的文档中得到了回答,
线程也可以在没有被通知,中断或超时的情况下被唤醒,即所谓的虚假唤醒。 虽然这在实践中很less发生,但是应用程序必须通过testing应该引起线程被唤醒的条件来防范它,并且如果条件不满足,则继续等待。 换句话说,等待应该总是发生在循环中,就像这样:
synchronized (obj) { while (<condition does not hold>) obj.wait(timeout); ... // Perform action appropriate to condition }
(有关此主题的更多信息,请参阅Doug Lea的“Java中的并行编程(第2版)”(Addison-Wesley,2000)中的第3.2.3节或Joshua Bloch的“Effective Java Programming Language Guide”(Addison- Wesley,2001)。
可能只有一个工人等待条件成为真实。
如果两个或两个以上的工人醒来(notifyAll),他们必须再次检查条件。 否则,即使可能只有其中一个数据,所有工作人员仍然会继续。
因为等待和通知用于实现[条件variables]( http://en.wikipedia.org/wiki/Monitor_ ( synchronization)#Blocking_condition_variables) ,所以你需要检查你正在等待的特定谓词是否为真持续。
我想我有@Gray的回答。
让我试着重新修改像我这样的新手,并请专家纠正我,如果我错了。
消费者同步块 ::
synchronized (queue) { // this needs to be while while (queue.isEmpty()) { queue.wait(); } queue.remove(); }
生产者同步块 ::
synchronized(queue) { // producer produces inside the queue queue.notify(); }
假设以下给定的顺序发生:
1)消费者#2进入消费者synchronized
块内,并且正在等待,因为队列是空的。
2)现在,生产者获得queue
上的锁并插入队列中并调用notify()。
现在,可以select消费者#1来运行正在等待queue
locking的第一次进入synchronized
块
要么
消费者#2可以select运行。
3)说,消费者#1被选中继续执行。 当它检查条件,它将是真实的,它将从队列中remove()
。
4)说,消费者#2正在从它停止执行的地方继续( wait()
方法之后的行)。 如果'while'条件不存在(而不是if
条件),它将继续调用remove()
,这可能会导致exception/意外行为。
为了后代,我想补充一点, while
循环如此重要的主要原因是线程之间的竞争条件。 例如:
synchronized (queue) { // this needs to be while while (queue.isEmpty()) { queue.wait(); } queue.remove(); }
使用上面的代码,可能有2个消费者线程。 当生产者locking要添加的queue
时,消费者#1可以在消费者#2已经在等待的synchronized
锁读取中进入。 当项目被添加到队列并notify
被调用时,#2被通知并且移动到运行队列,但是在#1消费者的后面等待queue
locking。 由于#1消费者首先从queue
remove
,如果while
循环只是一个if
,则会发生exceptionif
因为#2也会尝试调用remove
。 只是因为它被通知了,所以需要确保queue
由于竞争条件而仍然是空的。
这是有据可查的。 这是我刚才创build的一个网页,这个网页详细地解释了它,并且有一些示例代码。
http://256stuff.com/gray/docs/misc/producer_consumer_race_conditions/
当使用等待/通知机制时,安全性和活跃性都值得关注。 安全属性要求所有对象在multithreading环境中保持一致的状态。 活性属性要求每个操作或方法调用都能够不中断地完成。
为了保证活性,程序必须在调用wait()方法之前testingwhile循环的条件。 这个早期的testing检查另一个线程是否已经满足条件谓词并发送了一个通知。 发送通知后调用wait()方法将导致无限期阻塞。
为了保证安全,程序必须在wait()方法返回后testingwhile循环条件。 尽pipewait()旨在无限期地阻塞,直到收到通知,但仍必须将其封装在一个循环中以防止以下漏洞:
中间线程:第三个线程可以在发送通知和接收线程恢复执行的间隔期间获取共享对象的locking。 这第三个线程可以改变对象的状态,使其不一致。 这是一个检查时间,使用时间(TOCTOU)的竞争条件。
恶意通知:当条件谓词错误时,可以收到随机或恶意通知。 这样的通知会取消wait()方法。
未送达通知:线程在收到notifyAll()信号后执行的次序未指定。 因此,不相关的线程可以开始执行,并发现其条件谓词满足。 因此,尽pipe需要保持hibernate状态,仍然可以恢复执行。
虚假唤醒:某些Java虚拟机(JVM)实现容易受到虚假唤醒,导致即使没有通知也会等待线程唤醒。
由于这些原因,程序必须在wait()方法返回后检查条件谓词。 在调用wait()之前和之后,while循环是检查条件谓词的最佳select。
同样的,Condition接口的await()方法也必须在循环中调用。 根据Java API,接口条件
当等待一个条件,一个“虚拟唤醒”被允许发生,一般来说,作为底层平台语义的让步。 这对大多数应用程序没有什么实际的影响,因为条件应该总是等待循环,testing正在等待的状态谓词。 一个实现是免费的,以消除虚假唤醒的可能性,但build议应用程序员总是假设他们可能发生,所以总是等待循环。
新代码应该使用java.util.concurrent.locks并发实用程序来代替wait / notify机制。 但是,符合此规则的其他要求的遗留代码可以依赖于等待/通知机制。
不兼容的代码示例这个不兼容的代码示例在传统的if块中调用wait()方法,并且在收到通知后未能检查后置条件。 如果通知是偶然的或恶意的,线程可能会提前醒来。
synchronized (object) { if (<condition does not hold>) { object.wait(); } // Proceed when condition holds }
兼容的解决scheme此兼容的解决scheme在while循环中调用wait()方法,以在wait()调用之前和之后检查条件:
synchronized (object) { while (<condition does not hold>) { object.wait(); } // Proceed when condition holds }
java.util.concurrent.locks.Condition.await()方法的调用也必须包含在一个类似的循环中。
从你的问题:
我读过,我们应该总是从一个循环内调用wait():
尽pipewait()通常会等待,直到调用notify()或notifyAll()为止,但在非常罕见的情况下,由于虚假唤醒,等待的线程可能会被唤醒。 在这种情况下,等待的线程恢复,不通知notify()或notifyAll()被调用。
本质上,线程没有明显的原因恢复。
由于这种远程可能性,Oraclebuild议wait()的调用应该在检查线程正在等待的条件的循环中进行。