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


73

我读过,我们应该始终wait()在循环内调用a :

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

它可以正常工作而没有循环,那为什么呢?


5
注意:Oracle文档将此惯用法称为“保护块” docs.oracle.com/javase/tutorial/essential/concurrency/…–
Solomon Slow,

1
在琐碎的情况下,它只能在循环中“精细”工作。其余时间,您的程序被巧妙地破坏了。有关可能出现问题的示例,请参见《Java Concurrency in Practice》第14章。
布莱恩·格茨

作为澄清,我相信此评论有错别字,@ BrianGoetz的意思是“在琐碎的案件中,它只能“很好地”工作而没有循环”
Nathan Hughes

Answers:


76

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

例如,您正在等待一些东西:

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

邪恶的线程出现了:

theObjectYouAreWaitingOn.notifyAll();

如果邪恶线程没有/不能惹恼carryOn您,您只需继续等待适当的客户端。

编辑:添加了更多示例。等待可以中断。它会引发InterruptedException,您可能需要将等待结果包装在try-catch中。根据您的业务需求,您可以退出或取消该异常并继续等待。


5
这是正确的答案。等待文档:java.sun.com/javase/6/docs/api/java/lang/Object.html#wait(long) ...实际上描述了为什么需要将其放入循环-虚假唤醒。OP读过吗?
Andrew Duffy

啊,“虚假的唤醒”。记不得名字了。
akarnokd

1
您应该更新代码示例以在等待时处理ThreadInterruptedException。这将与您的答案一致。
罗宾

不知道 为InterruptedException添加try-catch似乎毫无意义,因为这取决于业务逻辑,以防线程中断。可能您只是退出,或者在循环内立即抑制它。
akarnokd

@AndrewDuffy我试图在我的答案中包括这一点...但似乎没人读过它:(
Adil

38

Object.wait(long milis)的文档中得到了答复

线程也可以唤醒,而不会被通知,中断或超时,即所谓的虚假唤醒。尽管实际上这种情况很少发生,但是应用程序必须通过测试应该导致线程唤醒的条件来防范它,并在条件不满足时继续等待。换句话说,等待应该总是在循环中发生,就像这样:

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

(有关此主题的更多信息,请参阅Doug Lea的“ Java并行编程(第二版)”(Addison-Wesley,2000年)中的3.2.3节,或Joshua Bloch的“有效的Java编程语言指南”(Addison-卫斯理,2001)。


17

为什么总是在循环内调用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使用者之后。这意味着,#1的消费者获得先往前走的呼叫remove()queue。如果while循环只是一个if,则当使用者#2在#1之后获得锁并调用时remove(),将发生异常,因为queue现在为空-另一个使用者线程已经删除了该项目。即使已收到通知,也必须确保queue由于这种竞争状况而肯定不为空。

这有据可查。这是我不久前创建的网页,其中详细解释了比赛情况并提供了一些示例代码。


1
那不是while (queue.isEmpty())没有!吗?
user104309'2013-10-29

除非消费者2退出同步块,否则可以认为消费者1为何会从队列中删除。无论如何,notify都是要通知使用者2,而使用者1将保持空闲状态。
jayendra bhatt

1
消费者#1保持空闲状态,但仍在等待锁定。当消耗#2收到通知时,它不会立即运行,因为它需要锁,因此它进入相同的等待队列,但#1 @jayendrabhatt之后。
格雷

1
您说:“仅由于已收到通知,因此需要确保队列由于此竞争条件而仍然为空”。我认为“尚不为空”应为“尚不为空”。
詹森·罗

12

可能只有一个工人等待条件变为现实。

如果两个或两个以上工人醒着(notifyAll),他们必须再次检查情况。否则,即使可能只有其中一个数据,所有工作人员仍将继续。


9

我想我得到了@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,它正在等待queuesynchronized首次进入该块

要么

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

3)说,选择Consumer#1继续执行。当它检查条件时,它将为true,并将remove()从队列中取出。

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


5

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


kd304是正确的-不仅可能未满足条件-这是线程可能从等待中虚假唤醒的事实
oxbow_lakes 2009年

@oxbow_lakes就涉及条件等待的线程而言,虚假唤醒和旨在发信号通知不同条件的通知之间没有真正的区别。无论哪种方式,您都必须检查谓词。
亚伦·曼帕

4

使用等待/通知机制时,安全性和活动性都是要考虑的问题。安全属性要求所有对象在多线程环境中保持一致的状态。liveness属性要求每个操作或方法调用都能完整执行而不会中断。

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

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

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

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

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

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

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

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

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

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

不兼容的代码示例 此不兼容的代码示例在传统的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()方法的调用包含在类似的循环中。


2

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

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) { }block内部运行。

互斥锁在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;
}

1

根据您的问题:

我读过我们应该总是从循环内调用wait():

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

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

由于这种可能性很小,Oracle建议在对线程进行等待检查的循环中进行对wait()的调用。


1
这个答案是错误的。这不是虚假唤醒的风险,这是wait应在循环中调用的原因。这是因为其他一些线程可能已经处理了该条件。
David Schwartz

0

人们会看到三件事:

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

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

  • 在循环中使用wait,在该循环测试中检查条件(不中断)。

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

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

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

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

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

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

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

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


-1

简单来说

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

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


这与wait功能无关
Simon Crane

您能解释一下吗?,我已经回答了为什么应该在循环内调用wait()
aravind
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.