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

Все мы знаем это для вызова Object.wait(), этот вызов должен быть помещен в синхронизируемый блок, иначе IllegalMonitorStateException брошен. Но какова причина того, чтобы сделать это ограничение? Я знаю это wait() выпускает монитор, но почему мы должны явно получить монитор путем создания конкретного блока синхронизируемым и затем выпустить монитор путем вызова wait()?

Что является потенциальным ущербом, если было возможно вызвать wait() вне синхронизируемого блока сохраняя это - семантика - приостановка потока вызывающей стороны?

250
задан Joachim Sauer 26 May 2013 в 21:00
поделиться

2 ответа

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

Семантически, вы никогда не можете просто wait(). Вам нужно, чтобы какое-то условие было выполнено, и если оно не выполнено, вы ждете, пока оно не будет выполнено. Так что на самом деле вы делаете следующее

if(!condition){
    wait();
}

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

И еще пара вещей, которые здесь не работают: то, что ваш поток прекратил ждать, не означает, что искомое условие истинно:

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

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

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

synchronized(lock){
    while(!condition){
        lock.wait();
    }
}

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

225
ответ дан 23 November 2019 в 02:56
поделиться

Каков потенциальный ущерб, если бы можно было вызывать wait() вне синхронизированного блока, сохраняя его семантику - приостановку вызывающего потока?

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

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

Первая попытка (без синхронизации) может выглядеть примерно так

class BlockingQueue {
    Queue<String> buffer = new LinkedList<String>();

    public void give(String data) {
        buffer.add(data);
        notify();                   // Since someone may be waiting in take!
    }

    public String take() throws InterruptedException {
        while (buffer.isEmpty())    // don't use "if" due to spurious wakeups.
            wait();
        return buffer.remove();
    }
}

Вот что потенциально может произойти:

  1. Поток-потребитель вызывает take() и видит, что buffer.isEmpty().

  2. Прежде чем поток-потребитель перейдет к вызову wait(), появляется поток-производитель и вызывает полный give(), то есть buffer.add(data); notify();

  3. Теперь поток-потребитель вызовет wait()пропустит только что вызванный notify()).

  4. Если не повезет, то поток-производитель не будет производить больше give() в результате того, что поток-потребитель так и не проснется, и мы получим dead-lock.

Как только вы поняли проблему, решение очевидно: используйте synchronized, чтобы убедиться, что notify никогда не вызывается между isEmpty и wait.

Не вдаваясь в подробности: Эта проблема синхронизации является универсальной. Как отмечает Майкл Боргвардт, wait/notify - это все о коммуникации между потоками, поэтому в конечном итоге вы всегда будете иметь состояние гонки, подобное описанному выше. Вот почему действует правило "только ждать внутри синхронизированных".


Абзац из ссылки, размещенной @Willie, достаточно хорошо резюмирует это:

Вам нужна абсолютная гарантия того, что ожидающий и уведомляющий согласны относительно состояния предиката. Официант проверяет состояние предиката в какой-то момент немного ДО того, как он засыпает, но его корректность зависит от того, что предикат истинен КОГДА он засыпает. Между этими двумя событиями есть период уязвимости, который может сломать программу.

Предикат, о котором должны договориться производитель и потребитель, в приведенном выше примере buffer.isEmpty(). А соглашение решается тем, что ожидание и уведомление выполняются в синхронизированных блоках.


Этот пост был переписан в виде статьи здесь: Java: Почему wait должен вызываться в синхронизированном блоке

273
ответ дан 23 November 2019 в 02:56
поделиться