Программирование JavaМногопоточностьJava-разработчик серверных приложений

В чём риск проверять условие перед wait без повторной проверки после пробуждения?

В чём риск проверять условие перед wait() без повторной проверки после пробуждения?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

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

Исторический контекст

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

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

Постановка проблемы

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

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

Подробное решение

Ожидание строят вокруг защищаемого условия: поток захватывает монитор, пока условие ложно, вызывает wait(), а после возврата снова проверяет условие. Вызов wait() атомарно освобождает монитор и переводит поток в состояние ожидания; перед возвратом поток повторно захватывает тот же монитор.

synchronized (lock) { while (!resourceAvailable()) { lock.wait(); } useResource(); } synchronized (lock) { makeResourceAvailable(); lock.notifyAll(); }

Цикл нужен по двум причинам: возможны ложные пробуждения, а также конкуренция между несколькими ожидающими потоками. notifyAll() будит всех ожидающих, но только один из них может первым захватить монитор и изменить состояние; остальные обязаны проверить условие заново.

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

На практике для новых задач предпочтительнее использовать высокоуровневые средства java.util.concurrent, например BlockingQueue, CountDownLatch или Condition. Они уменьшают объём ручного управления состоянием, но не отменяют необходимость понимать правило повторной проверки условия.

Ситуация из практики

В сервере несколько рабочих потоков ожидают задания. Изначально можно использовать wait/notify: это не требует дополнительных библиотек, но разработчик сам отвечает за монитор, условие, пробуждение и корректное завершение.

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

Оптимальным решением становится BlockingQueue: производители помещают задания, а потребители блокируются на извлечении при пустой очереди. Это убирает самописный цикл ожидания и делает правила публикации и пробуждения частью проверенного примитива; компромисс — необходимость принять модель очереди и её ограничения по ёмкости и политике остановки.

Что кандидаты часто упускают

  1. Считает ли notify() гарантией, что ожидающий поток немедленно продолжит выполнение?

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

  1. Почему даже при отсутствии ложных пробуждений нужен цикл?

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

  1. Что произойдёт, если вызвать wait() вне синхронизации по соответствующему объекту?

Поток получит IllegalMonitorStateException. Владение монитором необходимо, чтобы JVM могла атомарно связать освобождение монитора с постановкой потока на ожидание и затем корректно восстановить владение перед возвратом из wait(). Поэтому синхронизация должна использовать тот же объект, переданный в wait(), notify() или notifyAll().