Вопрос по multithreading, java – Почему wait () всегда вызывается внутри цикла

56

Я читал, что мы всегда должны называтьwait() из цикла:

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

Работает без петли, так почему?

Это работает только "хорошо" в цикле в тривиальных случаях. В остальное время ваша программа слегка ломается. Смотрите главу 14Java Concurrency in Practice примеры того, что может пойти не так. Brian Goetz
Примечание. Документация Oracle называет эту идиому «охраняемым блоком».docs.oracle.com/javase/tutorial/essential/concurrency/… Solomon Slow

Ваш Ответ

8   ответов
3

Безопасность и живучесть - это проблемы при использовании механизма ожидания / уведомления. Свойство безопасности требует, чтобы все объекты поддерживали согласованные состояния в многопоточной среде. Свойство liveness требует, чтобы каждая операция или вызов метода выполнялся до конца без прерывания.

Чтобы гарантировать жизнеспособность, программы должны проверить состояние цикла while перед вызовом метода wait (). Этот ранний тест проверяет, выполнил ли другой поток предикат условия и отправил ли уведомление. Вызов метода wait () после отправки уведомления приводит к неограниченной блокировке.

Чтобы гарантировать безопасность, программы должны проверять состояние цикла while после возврата из метода wait (). Хотя wait () предназначена для блокирования на неопределенный срок до получения уведомления, оно все равно должно быть заключено в цикл, чтобы предотвратить следующие уязвимости:

Thread in the middle: Третий поток может получить блокировку общего объекта в течение интервала между отправкой уведомления и получением потока, возобновляющего выполнение. Этот третий поток может изменить состояние объекта, оставив его несогласованным. Это состояние гонки с временем проверки (TOCTOU).

Malicious notification: Случайное или злонамеренное уведомление может быть получено, когда предикат условия ложен. Такое уведомление отменит метод wait ().

Misdelivered notification: Порядок выполнения потоков после получения сигнала notifyAll () не указан. Следовательно, несвязанный поток может начать выполнение и обнаружить, что его предикат условия удовлетворен. Следовательно, он может возобновить выполнение, несмотря на то, что он должен оставаться в бездействии

Spurious wakeups: Некоторые реализации виртуальной машины Java (JVM) уязвимы для ложных пробуждений, которые приводят к пробуждению ожидающих потоков даже без уведомления.

По этим причинам программы должны проверять предикат условия после возврата метода wait (). Цикл while - лучший выбор для проверки предиката условия как до, так и после вызова wait ().

Точно так же метод await () интерфейса Condition также должен вызываться внутри цикла. Согласно API Java, Условие интерфейса

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.

Новый код должен использовать утилиты параллелизма java.util.concurrent.locks вместо механизма ожидания / уведомления. Однако унаследованный код, соответствующий другим требованиям этого правила, может зависеть от механизма ожидания / уведомления.

Noncompliant Code Example Этот пример несовместимого кода вызывает метод wait () внутри традиционного блока if и не может проверить постусловие после получения уведомления. Если уведомление было случайным или злонамеренным, поток мог проснуться преждевременно.

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

Compliant Solution Это совместимое решение вызывает метод wait () из цикла while для проверки условия как до, так и после вызова wait ():

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

Вызовы метода java.util.concurrent.locks.Condition.await () также должны быть заключены в аналогичный цикл.

1

Из вашего вопроса:

I have read that we should always called a wait() from within a loop:

Хотя wait () обычно ожидает вызова notify () или notifyAll (), существует вероятность, что в очень редких случаях ожидающий поток может быть разбужен из-за ложного пробуждения. В этом случае ожидающий поток возобновляется без вызова notify () или notifyAll ().

По сути, тема возобновляется без видимой причины.

Из-за этой удаленной возможности Oracle рекомендует, чтобы вызовы wait () выполнялись в цикле, который проверяет условие ожидания потока.

Error: User Rate Limit ExceededwaitError: User Rate Limit Exceeded
10

Why should wait() always be called inside a loop

Основная причина, почему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 заблокировать, но это будетbehind потребитель # 1, который уже был заблокирован на замок. Это означает, что потребитель № 1 будет идти вперед, чтобы позвонитьremove() отqueue, Еслиwhile петля простоifтогда, когда потребитель # 2 получает блокировку после # 1 и вызываетremove()возникнет исключение, потому чтоqueue теперь пусто - другой потребительский поток уже удалил элемент. Только потому, что он был уведомлен, необходимо убедиться, чтоqueue все еще пусто из-за этого состояния гонки.

Это хорошо задокументировано. Вот веб-страница, которую я создал некоторое время назад, которая объясняетподробности гонки и имеет пример кода.

Error: User Rate Limit Exceeded
Error: User Rate Limit ExceededbehindError: User Rate Limit Exceeded
Error: User Rate Limit Exceeded
Error: User Rate Limit Exceededwhile (queue.isEmpty())Error: User Rate Limit Exceeded! ?
Error: User Rate Limit Exceeded
7

Я думаю, что получил ответ @Gray.

Позвольте мне перефразировать это для таких новичков, как я, и попросить экспертов исправить меня, если я ошибаюсь.

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();
}

Предположим, что в данном порядке происходит следующее:

1) потребитель № 2 попадает внутрь потребителяsynchronized блок и ожидает, так как очередь пуста.

2) Теперь производитель получает блокировку наqueueи вставляет в очередь и вызывает notify ().

Теперь любой потребитель № 1 может быть выбран для запуска, который ожидаетqueue заблокировать, чтобы войти вsynchronized блок в первый раз

или же

Потребитель № 2 может быть выбран для запуска.

3) скажем, потребитель № 1 выбран для продолжения исполнения. Когда он проверяет условие, это будет верно, и это будетremove() из очереди.

4) скажем, потребитель № 2 исходит из того места, где он остановил свое выполнение (строка послеwait() метод). Если «пока» условие не существует (вместоif условие), он просто продолжит звонитьremove() что может привести к исключению / неожиданному поведению.

36

документация для 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).

62

Вам нужно не только зациклить, но и проверить свое состояние в цикле. Java не гарантирует, что ваш поток будет разбужен только вызовом notify () / notifyAll () или правом notify () / notifyAll (). Из-за этого свойства версия без петель может работать в вашей среде разработки и неожиданно завершиться с ошибкой в производственной среде.

Например, вы чего-то ждете:

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

Злая нить приходит и:

theObjectYouAreWaitingOn.notifyAll();

Если злая нить не / не может связываться сcarryOn Вы просто продолжаете ждать правильного клиента.

Edit: Добавил еще несколько образцов. Ожидание может быть прервано. Выдает InterruptedException, и вам может понадобиться заключить ожидание в попытку. В зависимости от потребностей вашего бизнеса, вы можете выйти или подавить исключение и продолжить ожидание.

Error: User Rate Limit Exceeded
Error: User Rate Limit Exceeded
Error: User Rate Limit Exceeded
Error: User Rate Limit Exceededjava.sun.com/javase/6/docs/api/java/lang/Object.html#wait(long)Error: User Rate Limit Exceeded
Error: User Rate Limit Exceeded
5

ных] (http://en.wikipedia.org/wiki/Monitor_(synchronization)#Blocking_condition_variables) и поэтому вам нужно проверить, является ли конкретный предикат, которого вы ожидаете, истинным, прежде чем продолжить.

Error: User Rate Limit Exceeded
Error: User Rate Limit Exceeded
9

Если два или более рабочих проснулись (notifyAll), они должны снова проверить состояние. в противном случае все работники продолжили бы работу, хотя могли бы быть данные только для одного из них.

Похожие вопросы