为什么wait()总是在循环内调用

时间:2009-06-24 12:17:18

标签: java multithreading

我已经读过,我们应该始终在循环中调用wait()

while (!condition) { obj.wait(); }

没有循环就可以正常工作,为什么会这样?

11 个答案:

答案 0 :(得分:66)

您不仅需要循环它,还要检查循环中的条件。 Java不保证您的线程只能通过notify()/ notifyAll()调用或正确的notify()/ notifyAll()调用来唤醒。由于此属性,无环版本可能适用于您的开发环境,并且意外地在生产环境中失败。

例如,您正在等待某事:

synchronized (theObjectYouAreWaitingOn) {
   while (!carryOn) {
      theObjectYouAreWaitingOn.wait();
   }
}

邪恶的线索出现了:

theObjectYouAreWaitingOn.notifyAll();

如果邪恶的线程没有/不能弄乱carryOn你只是继续等待合适的客户。

修改:添加了更多样本。 等待可以中断。它抛出InterruptedException,你可能需要在try-catch中包装wait。根据您的业务需求,您可以退出或取消异常并继续等待。

答案 1 :(得分:37)

documentation for Object.wait(long milis)

中回答
  

线程也可以在没有被通知,中断或超时的情况下唤醒,即所谓的虚假唤醒。虽然这在实践中很少发生,但应用程序必须通过测试应该导致线程被唤醒的条件来防范它,并且如果条件不满足则继续等待。换句话说,等待应该总是出现在循环中,如下所示:

 synchronized (obj) {
     while (<condition does not hold>)
         obj.wait(timeout);
     ... // Perform action appropriate to condition
 }
  

(有关此主题的更多信息,   参见Doug Lea的第3.2.3节   “Java中的并发编程   (第二版)“(Addison-Wesley,   2000年,或约书亚布洛赫的第50项   “有效的Java编程语言   指南“(Addison-Wesley,2001)。

答案 2 :(得分:10)

  

为什么要始终在循环中调用wait()

while循环如此重要的主要原因是线程之间的竞争条件。当然,虚假的唤醒是真实的,对于某些架构来说它们很常见,但竞争条件更可能是while循环的原因。

例如:

synchronized (queue) {
    // this needs to be while
    while (queue.isEmpty()) {
       queue.wait();
    }
    queue.remove();
}

使用上面的代码,可能有2个消费者线程。当生产者锁定要添加的queue时,消费者#1可能会在synchronized锁定时被阻止,而消费者#2正在等待queue。当项目被添加到队列并由生产者调用notify时,#2将从等待队列中移除以在queue锁定上被阻止,但它将落后于已锁定锁定的#1消费者。这意味着排名第一的消费者可以先从remove()拨打queue。如果while循环只是if,那么当消费者#2在#1之后获得锁定并调用remove()时,会发生异常,因为queue现在为空 - 其他消费者线程已删除该项目。仅仅因为它已被通知,它需要确保queue因为这种竞争条件仍然是空的。

这有据可查。这是我之前创建的一个网页,它解释了race condition in detail并提供了一些示例代码。

答案 3 :(得分:9)

可能只有一名工人等待条件变为真。

如果两个或更多工人醒来(notifyAll),他们必须再次检查条件。 否则所有工人都会继续,即使其中一个人可能只有数据。

答案 4 :(得分:7)

我想我得到了@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()。

现在,可以选择消费者#1运行,等待queue锁第一次进入synchronized

消费者#2可以选择运行。

3)说,选择消费者#1继续执行。当它检查条件时,它将为真,它将从队列中remove()

4)说消费者#2正在从停止执行的地方开始(wait()方法之后的行)。如果&#39;而&#39;条件不存在(而不是if条件),它将继续调用remove(),这可能会导致异常/意外行为。

答案 5 :(得分:5)

因为wait和notify用于实现[条件变量](http://en.wikipedia.org/wiki/Monitor_(synchronization)#Blocking_condition_variables),所以你需要在继续之前检查你正在等待的特定谓词是否为真。

答案 6 :(得分:3)

使用等待/通知机制时,安全性和活跃性都受到关注。安全属性要求所有对象在多线程环境中保持一致的状态。 liveness属性要求每个操作或方法调用都不间断地执行完毕。

为了保证活跃,程序必须在调用wait()方法之前测试while循环条件。此早期测试检查另一个线程是否已满足条件谓词并发送通知。在发送通知后调用wait()方法会导致无限期阻塞。

为了保证安全性,程序必须在从wait()方法返回后测试while循环条件。虽然wait()旨在无限期地阻止,直到收到通知,但仍然必须将其封装在循环中以防止以下漏洞:

中间的线程:第三个线程可以在发送通知和接收线程恢复执行之间的间隔期间获取对共享对象的锁定。第三个线程可以更改对象的状态,使其不一致。这是一个检查时间,使用时间(TOCTOU)竞争条件。

恶意通知:条件谓词为false时,可以收到随机或恶意通知。这样的通知会取消wait()方法。

Misdelivered notification:未指定收到notifyAll()信号后线程执行的顺序。因此,不相关的线程可以开始执行并发现其条件谓词得到满足。因此,尽管需要保持休眠状态,它仍可以恢复执行。

虚假唤醒:某些Java虚拟机(JVM)实现容易受到虚假唤醒的影响,导致即使没有通知也会等待线程唤醒。

由于这些原因,程序必须在wait()方法返回后检查条件谓词。 while循环是在调用wait()之前和之后检查条件谓词的最佳选择。

类似地,还必须在循环内调用Condition接口的await()方法。根据Java API,接口条件

  

在等待条件时,允许“虚假唤醒”   通常,作为对底层平台的让步   语义。这对大多数应用程序几乎没有实际影响   程序作为条件应始终在循环中等待,   测试正在等待的状态谓词。一个   实施是免费的,以消除虚假唤醒的可能性   但建议应用程序员始终假设这一点   它们可以发生,因此总是在循环中等待。

新代码应使用java.util.concurrent.locks并发实用程序代替wait / notify机制。但是,允许符合此规则的其他要求的遗留代码依赖于等待/通知机制。

不符合规范的代码示例 这个不符合要求的代码示例在传统的if块中调用wait()方法,并且在收到通知后无法检查后置条件。如果通知是偶然的或恶意的,线程可能会过早醒来。

synchronized (object) {
  if (<condition does not hold>) {
    object.wait();
  }
  // Proceed when condition holds
}

合规解决方案 这个兼容的解决方案从while循环中调用wait()方法来检查wait()调用之前和之后的条件:

synchronized (object) {
  while (<condition does not hold>) {
    object.wait();
  }
  // Proceed when condition holds
}

java.util.concurrent.locks.Condition.await()方法的调用也必须包含在类似的循环中。

答案 7 :(得分:1)

来自你的问题:

  

我已经读过我们应该总是在循环中调用wait():

尽管wait()通常会等到调用notify()或notifyAll(),但在极少数情况下,由于虚假的唤醒,可能会唤醒等待的线程。在这种情况下,等待线程会在没有调用notify()或notifyAll()的情况下恢复。

从本质上讲,线程没有明显的原因恢复。

由于这种远程可能性,Oracle建议在一个循环中调用wait(),该循环检查线程正在等待的条件。

答案 8 :(得分:1)

在获得答案之前,让我们先看看如何实现等待。

wait(mutex) {
   // automatically release mutex
   // and go on wait queue

   // ... wait ... wait ... wait ...

   // remove from queue
   // re-acquire mutex
   // exit the wait operation
}

在您的示例中,mutexobj,并假设您的代码在synchronized(obj) { }块中运行。

互斥锁在Java中被称为监视器[尽管有些细微差别]

使用条件变量和if的并发示例

synchronized(obj) {
  if (!condition) { 
    obj.wait(); 
  }
  // Do some stuff related to condition
  condition = false;
}

让我们说我们有2个线程。 线程1 线程2 。 让我们沿着时间轴看到一些状态。

在t = x

线程1状态

等待... wait ... wait ... wait ..

线程2状态

只需进入同步部分,因为根据线程1的状态,互斥体/监视器将被释放。

您可以在java.sun.com/javase/6/docs/api/java/lang/Object.html#wait(long)上阅读有关wait()的更多信息。

这是唯一一件很难理解的事情。当1个线程位于同步块内时。另一个线程仍可以进入同步块,因为wait()导致监视器/互斥体被释放。

线程2即将读取if (!condition)语句。

在t = x + 1

notify()由该互斥锁/监视器上的某个线程触发。

condition变为true

线程1状态:

正在等待re-acquire mutex,[由于线程2现在已锁定]

线程2状态:

如果有条件则不要进入并标记condition = false

在t = x + 2

线程1状态:

退出等待操作并即将标记condition = false

此状态不一致,因为condition应该是true,但已经是false,因为线程2 标记了它false之前。

这就是原因,需要while而不是if。由于while会触发再次检查thread 1的条件,因此线程1将再次开始等待。

结果

为了避免这种不一致,正确的代码看起来像这样:

synchronized(obj) {
  while (!condition) { 
    obj.wait(); 
  }
  // Do some stuff related to condition
  condition = false;
}

答案 9 :(得分:0)

人们将看到的三件事:

  • 使用等待而不检查任何内容(已损坏)

  • 使用带有条件的等待,首先进行if检查(BROKEN)。

  • 使用循环等待,其中循环测试将检查条件(不中断)。

不了解有关等待和通知工作的这些细节会导致人们选择错误的方法:

  • 一个是线程不记得在等待之前发生的通知。如果线程在运气不好的时候没有在等待,则notify和notifyAll方法只会影响已经在等待的线程。

  • 另一个是线程一旦开始等待就释放锁。收到通知后,它将重新获取锁,并从上次停止的地方继续。释放锁定意味着线程一旦唤醒就不知道当前状态,因此从那时起任何其他线程都可以进行更改。在线程开始等待之前进行的检查不会告诉您有关当前状态的任何信息。

因此,第一种情况,不进行检查,会使您的代码容易受到竞争条件的影响。如果一个线程比另一个线程有更多的领先优势,这可能偶然发生。否则您可能会永远等待线程。如果您超时,那么最终会得到慢速的代码,有时可能无法满足您的要求。

除了通知本身之外,添加条件进行检查可以保护您的代码免受这些竞争条件的影响,并使您的代码可以知道状态,即使线程没有在正确的时间等待。

如果只有2个线程,则第二种情况带有if-checks可能有效。这就限制了事物可以进入的状态数量,并且当您做出错误的假设时,您不会被严重烧毁。这是很多玩具示例代码练习的情况。结果是人们在真正不懂的时候就以为自己理解了。

提示:现实世界中的代码有两个以上的线程。

使用循环可让您在重新获取锁定后重新检查条件,以便根据当前状态(而不是陈旧状态)前进。

答案 10 :(得分:-1)

简单地说,

'if'是一个条件语句,一旦条件满足,剩下的代码块将被执行。

'while'是一个循环,它将检查条件,除非不满足条件。