Почему wait () всегда вызывается внутри цикла - PullRequest
60 голосов
/ 24 июня 2009

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

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

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

Ответы [ 8 ]

66 голосов
/ 24 июня 2009

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

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

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

Появляется злая нить и:

theObjectYouAreWaitingOn.notifyAll();

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

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

37 голосов
/ 24 июня 2009

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

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

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

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

11 голосов
/ 10 апреля 2012

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

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

9 голосов
/ 24 июня 2009

Может быть более одного работника, ожидающего выполнения условия.

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

8 голосов
/ 29 октября 2016

Я думаю, что получил ответ @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(), что может привести к исключению / неожиданному поведению.

5 голосов
/ 24 июня 2009

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

3 голосов
/ 22 июня 2016

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

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

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

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

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

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

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

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

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

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

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

Пример несовместимого кода Этот пример несовместимого кода вызывает метод 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 () также должны быть заключены в аналогичный цикл.

1 голос
/ 10 декабря 2015

От вашего вопроса:

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

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

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

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

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