简体   繁体   English

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

[英]Why should wait() always be called inside a loop

I have read that we should always call a wait() from within a loop:我读过我们应该始终从循环中调用wait()

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

It works fine without a loop so why is that?它在没有循环的情况下工作正常,为什么会这样?

You need not only to loop it but check your condition in the loop.您不仅需要循环它,还需要检查循环中的条件。 Java does not guarantee that your thread will be woken up only by a notify()/notifyAll() call or the right notify()/notifyAll() call at all. Java 不保证您的线程只会被notify()/notifyAll() 调用或正确的notify()/notifyAll() 调用唤醒。 Because of this property the loop-less version might work on your development environment and fail on the production environment unexpectedly.由于此属性,无循环版本可能会在您的开发环境中运行,而在生产环境中会意外失败。

For example, you are waiting for something:例如,您正在等待某事:

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

An evil thread comes along and:一个邪恶的线程出现了:

theObjectYouAreWaitingOn.notifyAll();

If the evil thread does not/can not mess with the carryOn you just continue to wait for the proper client.如果邪恶的线程没有/不能弄乱carryOn您只需继续等待正确的客户端。

Edit: Added some more samples.编辑:添加了更多示例。 The wait can be interrupted.可以中断等待。 It throws InterruptedException and you might need to wrap the wait in a try-catch.它抛出 InterruptedException 并且您可能需要将等待包装在 try-catch 中。 Depending on your business needs, you can exit or suppress the exception and continue waiting.根据您的业务需求,您可以退出或抑制异常并继续等待。

It's answered in documentation for Object.wait(long milis)它在Object.wait(long milis) 的文档中得到了回答

A thread can also wake up without being notified, interrupted, or timing out, a so-called spurious wakeup.线程也可以在没有被通知、中断或超时的情况下唤醒,即所谓的虚假唤醒。 While this will rarely occur in practice, applications must guard against it by testing for the condition that should have caused the thread to be awakened, and continuing to wait if the condition is not satisfied.虽然这在实践中很少发生,但应用程序必须通过测试应该导致线程被唤醒的条件来防止它,如果条件不满足则继续等待。 In other words, waits should always occur in loops, like this one:换句话说,等待应该总是在循环中发生,就像这样:

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

(For more information on this topic, see Section 3.2.3 in Doug Lea's "Concurrent Programming in Java (Second Edition)" (Addison-Wesley, 2000), or Item 50 in Joshua Bloch's "Effective Java Programming Language Guide" (Addison-Wesley, 2001). (有关此主题的更多信息,请参阅 Doug Lea 的“Java 中的并发编程(第二版)”(Addison-Wesley,2000)中的第 3.2.3 节,或 Joshua Bloch 的“Effective Java Programming Language Guide”(Addison-韦斯利,2001 年)。

Why should wait() always be called inside a loop为什么总是在循环内调用 wait()

The primary reason why while loops are so important is race conditions between threads. while循环如此重要的主要原因是线程之间的竞争条件。 Certainly spurious wakeups are real and for certain architectures they are common, but race conditions are a much more likely reason for the while loop.当然,虚假唤醒是真实的,并且对于某些架构它们很常见,但竞争条件更可能是while循环的原因。

For example:例如:

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

With the above code, there may be 2 consumer threads.用上面的代码,可能有2个消费者线程。 When the producer locks the queue to add to it, consumer #1 may be blocked at the synchronized lock while consumer #2 is waiting on the queue .当生产者锁定queue以添加到queue时,消费者 #1 可能在synchronized锁处被阻塞,而消费者 #2 正在等待queue When the item is added to the queue and notify called by the producer, #2 is moved from the wait queue to be blocked on the queue lock, but it will be behind the #1 consumer which was already blocked on the lock.当项目被添加到队列并由生产者调用notify ,#2 从等待队列中移动到queue锁上被阻塞,但它会落后于已经被锁阻塞的 #1 消费者。 This means that the #1 consumer gets to go forward first to call remove() from the queue .这意味着 #1 消费者首先可以从queue调用remove() If the while loop is just an if , then when consumer #2 gets the lock after #1 and calls remove() , an exception would occur because the queue is now empty -- the other consumer thread already removed the item.如果while循环只是一个if ,那么当消费者 #2 在 #1 之后获得锁并调用remove() ,会发生异常,因为queue现在是空的——另一个消费者线程已经删除了该项目。 Even though it was notified, it needs to be make sure the queue is for sure not empty because of this race condition.即使它被通知了,它也需要确保queue肯定不会因为这种竞争条件而为空。

This well documented.这有据可查。 Here's a web page I created a while back which explains the race condition in detail and has some sample code.这是我不久前创建的一个网页,它详细解释了竞争条件并有一些示例代码。

There might be more then just one worker waiting for a condition to become true.可能有不止一个工人在等待条件变为真。

If two or more worker get awake (notifyAll) they have to check the condition again.如果两个或更多工人醒来(notifyAll),他们必须再次检查情况。 otherwise all workers would continue even though there might only be data for one of them.否则所有工人都会继续工作,即使可能只有其中一位工人的数据。

I think I got @Gray 's answer.我想我得到了@Gray 的答案。

Let me try to rephrase that for newbies like me and request the experts to correct me if I am wrong.让我试着为像我这样的新手改写一下,如果我错了,请专家纠正我。

Consumer synchronized block: :消费者同步块

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

Producer synchronized block: :生产者同步块::

synchronized(queue) {
 // producer produces inside the queue
    queue.notify();
}

Assume the following happens in the given order:假设以下按给定的顺序发生:

1) consumer#2 gets inside the consumer synchronized block and is waiting since queue is empty. 1) 消费者#2 进入消费者synchronized块并等待,因为队列为空。

2) Now, producer obtains the lock on queue and inserts inside the queue and calls notify(). 2) 现在,生产者获得queue锁并插入队列并调用notify()。

Now,either consumer#1 can be chosen to run which is waiting for queue lock to enter the synchronized block for the first time现在,可以选择消费者#1 运行它正在等待queue锁第一次进入synchronized

or要么

consumer#2 can be chosen to run.可以选择消费者#2 运行。

3) say, consumer#1 is chosen to continue with the execution. 3) 说,消费者#1 被选择继续执行。 When it checks the condition,it will be true and it will remove() from the queue.当它检查条件时,它将为真,并将从队列中remove()

4) say,consumer#2 is proceeding from where it halted its execution (the line after the wait() method). 4) 说,consumer#2 从它停止执行的地方开始( wait()方法之后的那一行)。 If 'while' condition is not there (instead an if condition), it will just proceed to call remove() which might result in an exception/unexpected behaviour.如果“while”条件不存在(而不是if条件),它将继续调用remove() ,这可能会导致异常/意外行为。

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

Both safety and liveness are concerns when using the wait/notify mechanism.使用等待/通知机制时,安全性和活性都是需要考虑的问题。 The safety property requires that all objects maintain consistent states in a multithreaded environment.安全属性要求所有对象在多线程环境中保持一致的状态。 The liveness property requires that every operation or method invocation execute to completion without interruption. liveness 属性要求每个操作或方法调用都执行到完成而不会中断。

To guarantee liveness, programs must test the while loop condition before invoking the wait() method.为了保证活性,程序必须在调用 wait() 方法之前测试 while 循环条件。 This early test checks whether another thread has already satisfied the condition predicate and sent a notification.这个早期测试检查另一个线程是否已经满足条件谓词并发送通知。 Invoking the wait() method after the notification has been sent results in indefinite blocking.在发送通知后调用 wait() 方法会导致无限期阻塞。

To guarantee safety, programs must test the while loop condition after returning from the wait() method.为了保证安全,程序必须在从 wait() 方法返回后测试 while 循环条件。 Although wait() is intended to block indefinitely until a notification is received, it still must be encased within a loop to prevent the following vulnerabilities:尽管 wait() 旨在无限期地阻塞直到收到通知,但它仍然必须包含在循环中以防止以下漏洞:

Thread in the middle: A third thread can acquire the lock on the shared object during the interval between a notification being sent and the receiving thread resuming execution.中间线程第三个线程可以在发送通知和接收线程恢复执行之间的时间间隔内获取共享对象上的锁。 This third thread can change the state of the object, leaving it inconsistent.这第三个线程可以更改对象的状态,使其不一致。 This is a time-of-check, time-of-use (TOCTOU) race condition.这是一个检查时间、使用时间 (TOCTOU) 竞争条件。

Malicious notification: A random or malicious notification can be received when the condition predicate is false.恶意通知:当条件谓词为假时,可以收到随机或恶意通知。 Such a notification would cancel the wait() method.这样的通知将取消 wait() 方法。

Misdelivered notification: The order in which threads execute after receipt of a notifyAll() signal is unspecified.错误传递通知:未指定线程在收到 notifyAll() 信号后执行的顺序。 Consequently, an unrelated thread could start executing and discover that its condition predicate is satisfied.因此,一个不相关的线程可以开始执行并发现它的条件谓词得到满足。 Consequently, it could resume execution despite being required to remain dormant.因此,尽管需要保持休眠状态,但它仍可以恢复执行。

Spurious wakeups: Certain Java Virtual Machine (JVM) implementations are vulnerable to spurious wakeups that result in waiting threads waking up even without a notification.虚假唤醒某些 Java 虚拟机 (JVM) 实现容易受到虚假唤醒的影响,导致等待线程在没有通知的情况下唤醒。

For these reasons, programs must check the condition predicate after the wait() method returns.由于这些原因,程序必须在 wait() 方法返回后检查条件谓词。 A while loop is the best choice for checking the condition predicate both before and after invoking wait(). while 循环是在调用 wait() 之前和之后检查条件谓词的最佳选择。

Similarly, the await() method of the Condition interface also must be invoked inside a loop.同样,Condition 接口的 await() 方法也必须在循环内调用。 According to the Java API, Interface Condition根据 Java API,接口条件

When waiting upon a Condition, a "spurious wakeup" is permitted to occur, in general, as a concession to the underlying platform semantics.在等待条件时,通常允许发生“虚假唤醒”,作为对底层平台语义的让步。 This has little practical impact on most application programs as a Condition should always be waited upon in a loop, testing the state predicate that is being waited for.这对大多数应用程序几乎没有实际影响,因为应始终在循环中等待条件,以测试正在等待的状态谓词。 An implementation is free to remove the possibility of spurious wakeups but it is recommended that applications programmers always assume that they can occur and so always wait in a loop.实现可以自由地消除虚假唤醒的可能性,但建议应用程序程序员始终假设它们可能发生,因此始终在循环中等待。

New code should use the java.util.concurrent.locks concurrency utilities in place of the wait/notify mechanism.新代码应使用 java.util.concurrent.locks 并发实用程序代替等待/通知机制。 However, legacy code that complies with the other requirements of this rule is permitted to depend on the wait/notify mechanism.但是,允许符合此规则其他要求的遗留代码依赖于等待/通知机制。

Noncompliant Code Example This noncompliant code example invokes the wait() method inside a traditional if block and fails to check the postcondition after the notification is received.不符合要求的代码示例这不符合要求的代码示例调用内部的传统如果块的等待()方法和失败时,接收到该通知后,检查后置条件。 If the notification were accidental or malicious, the thread could wake up prematurely.如果通知是意外的或恶意的,线程可能会过早唤醒。

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

Compliant Solution This compliant solution calls the wait() method from within a while loop to check the condition both before and after the call to wait():合规解决方案此合规解决方案从 while 循环内调用 wait() 方法以检查调用 wait() 之前和之后的条件:

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

Invocations of the java.util.concurrent.locks.Condition.await() method also must be enclosed in a similar loop. java.util.concurrent.locks.Condition.await() 方法的调用也必须包含在类似的循环中。

Before getting to the answer, lets see how wait is probably implemented.在得到答案之前,让我们看看等待可能是如何实现的。

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

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

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

In your example mutex is the obj with the assumption that your code is running inside synchronized(obj) { } block.在您的示例中, mutexobj ,假设您的代码在synchronized(obj) { }块内运行。

A mutex is called as monitor in Java [some subtle differences though]互斥体在 Java 中被称为监视器 [尽管有一些细微的差异]

A concurrency example using condition variable with if使用条件变量和if并发示例

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

Lets say we have 2 threads.假设我们有 2 个线程。 Thread 1 and Thread 2 .线程 1线程 2 Lets see some states along the timeline.让我们看看时间线上的一些状态。

at t = x在 t = x

Thread 1 state :线程 1 状态

waiting on ... wait ... wait ... wait .. ... wait ... wait ... wait ..

Thread 2 state :线程 2 状态

Just entered the synchronised section, since as per the thread 1's state, the mutex/monitor is released.刚刚进入同步部分,因为根据线程1的状态,互斥锁/监视器被释放。

You can read more about wait() here java.sun.com/javase/6/docs/api/java/lang/Object.html#wait(long) .您可以在此处阅读有关 wait() 的更多信息java.sun.com/javase/6/docs/api/java/lang/Object.html#wait(long)

This is the only thing that is tricky to understand.这是唯一难以理解的事情。 When 1 thread is inside the synchronized block.当 1 个线程在同步块内时。 Another thread can still enter the synchronized block because wait() causes the monitor/mutex to be released.另一个线程仍然可以进入同步块,因为 wait() 导致监视器/互斥体被释放。

Thread 2 is about to read if (!condition) statement.线程 2 即将读取if (!condition)语句。

at t = x + 1在 t = x + 1

notify() is triggered by some thread on this mutex/monitor. notify() 由此互斥锁/监视器上的某个线程触发。

condition becomes true condition true

Thread 1 state:线程 1 状态:

Waiting at re-acquire mutex , [Since thread-2 has the lock now]等待re-acquire mutex ,[因为线程 2 现在有锁]

Thread 2 state:线程 2 状态:

Doesn't go inside if condition and marks the condition = false .不进入 if 条件并标记condition = false

at t = x + 2在 t = x + 2

Thread 1 state:线程 1 状态:

Exits the wait operation and about to mark condition = false .退出等待操作并即将标记condition = false

This state is inconsistent as condition is supposed to be true but is false already, because thread 2 marked it false previously.此状态不一致,因为condition应该为true但已经为false ,因为线程 2 之前将其标记为false

And thats the reason, while is required instead of if .这就是原因, while是必需的, while不是if As while would trigger the condition to be checked again for thread 1 and thread 1 will begin waiting again.因为while会触发再次检查thread 1 ,线程 1 将再次开始等待。

Result结果

In order to avoid this inconsistency the correct code seems to be like this:为了避免这种不一致,正确的代码似乎是这样的:

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

From your Question:从你的问题:

I have read that we should always called a wait() from within a loop:我读过我们应该始终从循环中调用 wait() :

Although wait( ) normally waits until notify( ) or notifyAll( ) is called, there is a possibility that in very rare cases the waiting thread could be awakened due to a spurious wakeup.尽管wait() 通常会等到notify() 或notifyAll() 被调用,但在极少数情况下,等待线程可能会由于虚假唤醒而被唤醒。 In this case, a waiting thread resumes without notify( ) or notifyAll( ) having been called.在这种情况下,等待线程在没有调用 notify() 或 notifyAll() 的情况下恢复。

In essence, the thread resumes for no apparent reason.从本质上讲,线程会无缘无故地恢复。

Because of this remote possibility, Oracle recommends that calls to wait( ) should take place within a loop that checks the condition on which the thread is waiting.由于这种可能性很小,Oracle 建议对 wait() 的调用应在检查线程等待条件的循环中进行。

Three things you will see people do:你会看到人们做的三件事:

  • Using wait without checking anything (BROKEN)使用等待而不检查任何内容(BROKEN)

  • Using wait with a condition, using an if check first (BROKEN).使用带条件的等待,首先使用 if 检查 (BROKEN)。

  • Using wait in a loop, where the loop test checks the condition (NOT BROKEN).在循环中使用等待,其中循环测试检查条件(未中断)。

Not appreciating these details about how wait and notify work leads people to choose the wrong approach:不了解等待和通知工作如何导致人们选择错误方法的这些细节:

  • One is that a thread doesn't remember notifications that happened before it got around to waiting.一个是线程不记得在等待之前发生的通知。 The notify and notifyAll methods only effect threads that are already waiting, if a thread isn't waiting at the time it is out of luck. notify 和 notifyAll 方法只影响已经在等待的线程,如果一个线程没有在等待,那么它就不走运了。

  • Another is that a thread releases the lock once it starts waiting.另一个是线程一旦开始等待就会释放锁。 Once it gets a notification it re-acquires the lock and continues on where it left off.一旦收到通知,它会重新获取锁定并从停止的地方继续。 Releasing the lock means that thread does not know anything about the current state once it wakes back up, any number of other threads could have made changes since then.解除锁定意味着线程不知道目前的状态的任何事情,一旦被唤醒,任何数量的其他线程可能从那时起所做的更改。 The check made before the thread started waiting doesn't tell you anything about what the state is currently.在线程开始等待之前所做的检查不会告诉您有关当前状态的任何信息。

So the first case, with no checking, lays your code vulnerable to race conditions.因此,没有检查的第一种情况会使您的代码容易受到竞争条件的影响。 It might happen to work by accident if one thread has enough of a head start over another.如果一个线程比另一个线程有足够的领先优势,它可能会偶然工作。 Or you may have threads waiting forever.或者你可能让线程永远等待。 If you sprinkle in timeouts then you end up with slow code that sometimes doesn't do what you want.如果你加入超时,那么你最终会得到缓慢的代码,有时不会做你想要的。

Adding a condition to check apart from the notification itself protects your code from these race conditions and gives your code a way to know what the state is even if the thread wasn't waiting at the right time.在通知本身之外添加一个条件来检查可以保护您的代码免受这些竞争条件的影响,并为您的代码提供一种方法来了解状态是什么,即使线程没有在正确的时间等待。

The second case, with if-checks, is likely to work if you have only 2 threads.如果您只有 2 个线程,则使用 if 检查的第二种情况可能会起作用。 That puts a limit on the number of states things can get into and when you made faulty assumptions you don't get burned so badly.这限制了事物可以进入的状态数量,并且当您做出错误的假设时,您不会受到如此严重的影响。 This is the situation for lots of toy example code exercises.这是许多玩具示例代码练习的情况。 The result is people come away thinking they understand, when they really don't.结果是人们离开时以为他们理解了,但实际上他们并不理解。

Protip: Real world code has more than two threads.提示:现实世界的代码有两个以上的线程。

Using the loop lets you re-check the condition once you re-acquire the lock so that you're moving forward based on current state, not on stale state.使用循环可以让您在重新获取锁定后重新检查条件,以便您根据当前状态而不是陈旧状态继续前进。

In simple words,简单来说,

'if' is a conditional statement , once condition is satisfied remaining block of code will get executed. 'if' 是一个条件语句,一旦满足条件,剩余的代码块将被执行。

'while' is a loop which going check the condition unless condition is not satisfied. 'while' 是一个循环,它会检查条件,除非条件不满足。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM