Подтвердить что ты не робот

Почему wait() должен всегда вызываться внутри цикла

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

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

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

4b9b3361

Ответ 1

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

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

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

Происходит злая нить и:

theObjectYouAreWaitingOn.notifyAll();

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

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

Ответ 2

Он ответил в документации для Object.wait(long milis)

Поток также может проснуться, не будучи уведомленным, прерванным или тайм-аутом, так называемым ложным пробуждением. Хотя это редко случается на практике, приложения должны защищать его, проверяя условие, которое должно было вызвать пробуждение потока, и продолжая ждать, если условие не будет выполнено. Другими словами, ожидания всегда должны встречаться в циклах, например:

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

(Для получения дополнительной информации по этой теме, см. раздел 3.2.3 в книге Дуга Ли "Параллельное программирование в Java (Второе издание)" (Эддисон-Уэсли, 2000), или Пункт 50 в книге Джошуа Блоха "Эффективный язык программирования Java Guide" (Addison-Wesley, 2001).

Ответ 3

  Почему 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 все еще пуст из-за этого состояния гонки.

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

Ответ 4

Тогда может быть больше одного работника, ожидающего, когда условие станет истинным.

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

Ответ 5

Я думаю, что получил ответ @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()). Если условие "while" не существует (вместо условия if), оно просто перейдет к вызову remove(), что может привести к возникновению исключений/неожиданностей.

Ответ 6

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

Ответ 7

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

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

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

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

Вредоносные уведомления. Если предикат условия является ложным, может быть получено случайное или вредоносное уведомление. Такое уведомление отменяет метод wait().

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

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

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

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

При ожидании условия "ложное пробуждение" разрешено происходят, в общем, как уступка базовой платформе семантика. Это мало влияет на большинство приложений программ как условие всегда следует ждать в цикле, проверяя предикат состояния, которого ждут. реализация может бесплатно удалить возможность побочных пробуждений но рекомендуется, чтобы программисты приложений всегда предполагали, что они могут возникать и поэтому всегда ждать в цикле.

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

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

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

Соответствующее решение Это совместимое решение вызывает метод wait() из цикла while, чтобы проверить условие до и после вызова wait():

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

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

Ответ 8

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

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

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

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

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

Ответ 9

Три вещи, которые вы увидите, люди делают:

  • Использование ожидания без проверки (BROKEN)

  • Использование wait с условием, сначала проверка if (BROKEN).

  • Использование ожидания в цикле, где проверка цикла проверяет условие (НЕ РАЗРЫВ).

Не принимая во внимание эти подробности о том, как ожидание и уведомление о работе приводит к тому, что люди выбирают неправильный подход:

  • Одна из них заключается в том, что поток не запоминает уведомления, которые произошли до того, как он успел дождаться. Методы notify и notifyAll воздействуют только на потоки, которые уже ожидают, если поток не ждет в тот момент, когда ему не повезло.

  • Другое - поток снимает блокировку, как только начинает ждать. Как только он получает уведомление, он повторно получает блокировку и продолжает на том месте, где остановился. Снятие блокировки означает, что поток не знает ничего о текущем состоянии после восстановления, любое число других потоков могло бы внести изменения с тех пор. Проверка, выполненная до того, как поток начал ждать, ничего не говорит о том, в каком состоянии он находится в настоящее время.

Таким образом, первый случай, без проверки, делает ваш код уязвимым для условий гонки. Это может случиться так, что сработает случайно, если у одного потока будет достаточное преимущество перед другим. Или у вас могут быть темы, ожидающие вечно. Если вы посыпаете время ожидания, то в итоге получается медленный код, который иногда не делает то, что вы хотите.

Добавление условия для проверки, помимо самого уведомления, защищает ваш код от этих условий гонки и дает вашему коду возможность узнать, что такое состояние, даже если поток не ожидал в нужное время.

Второй случай, с проверками if, скорее всего, будет работать, если у вас только 2 потока. Это накладывает ограничение на количество состояний, в которые можно попасть, и когда вы делаете ошибочные предположения, вы не так сильно обожгетесь. Так обстоит дело с большим количеством упражнений с примерами кода. В результате люди уходят, думая, что понимают, а на самом деле - нет.

Подсказка: в реальном мире кода более двух потоков.

Использование цикла позволяет вам повторно проверить условие после повторного захвата блокировки, чтобы вы двигались вперед в зависимости от текущего состояния, а не устаревшего состояния.