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

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

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

Ответ 1

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

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

if(!condition){
    wait();
}

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

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

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

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

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

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

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

Ответ 2

Каков потенциальный ущерб, если было возможно вызвать 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();
    }
}

Это может произойти:

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

  • До того, как потребительский поток продолжит вызов wait(), поток производителей появится и вызовет полный give(), то есть buffer.add(data); notify();

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

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

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

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


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

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

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


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

Ответ 3

@Роллербол прав. Вызывается wait(), поэтому поток может ждать появления некоторого условия, когда этот вызов wait() происходит, поток вынужден отказаться от его блокировки.
Чтобы отказаться от чего-то, вам нужно владеть им в первую очередь. Нить должна сначала иметь замок. Следовательно, необходимо вызвать его внутри метода/блока synchronized.

Да, я согласен со всеми приведенными выше ответами относительно возможных повреждений/несоответствий, если вы не проверяли условие в методе/блоке synchronized. Однако, как указывал @shrini1000, просто вызов wait() внутри синхронизированного блока не предотвратит эту несогласованность.

Вот хороший читать..

Ответ 4

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

  • Если первый поток переходит в makeChangeOnX() и проверяет условие while, и он true (x.metCondition() возвращает false, значит x.condition is false), чтобы он попал внутрь него. Затем перед методом wait() другой поток переходит в setConditionToTrue() и устанавливает x.condition в true и notifyAll().
  • Тогда только после этого 1-й поток войдет в свой метод wait() (не затронутый notifyAll(), который произошел за несколько минут до этого). В этом случае 1-й поток будет ждать, пока другой поток выполнит setConditionToTrue(), но это может не произойти снова.

Но если вы ставите synchronized перед методами, которые изменяют состояние объекта, этого не произойдет.

class A {

    private Object X;

    makeChangeOnX(){
        while (! x.getCondition()){
            wait();
            }
        // Do the change
    }

    setConditionToTrue(){
        x.condition = true; 
        notifyAll();

    }
    setConditionToFalse(){
        x.condition = false;
        notifyAll();
    }
    bool getCondition(){
        return x.condition;
    }
}

Ответ 5

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

boolean wasNotified = false;
while(!wasNotified) {
    wait();
}

Затем уведомление наборов потоков былоNotified переменной в true и уведомлять.

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

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

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

synchronized(monitor) {
    boolean wasNotified = false;
    while(!wasNotified) {
        wait();
    }
}

Спасибо, надеюсь, он уточнит.

Ответ 6

непосредственно из this java oracle tutorial:

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

Ответ 7

В основном это связано с аппаратной архитектурой (т.е. ОЗУ и кеша).

Если вы не используете synchronized вместе с wait() или notify(), другой поток может войти в тот же блок, а не ждать, пока монитор не войдет в него. Кроме того, когда, например, доступ к массиву без синхронизированного блока, другой поток может не увидеть изменения к нему... на самом деле другой поток не увидит никаких изменений в нем, когда у него уже есть копия массива в кэше уровня x (aka 1st/2nd/Кэши 3-го уровня) потока ядра процессора.

Но синхронизированные блоки - это только одна сторона медали: если вы действительно обращаетесь к объекту в синхронизированном контексте из несинхронизированного контекста, объект все равно не будет синхронизироваться даже в синхронизированном блоке, поскольку он содержит собственный копия объекта в кеше. Я писал об этом здесь: fooobar.com/questions/32865/... и Если блокировка содержит объект, не являющийся конечным, может ли ссылка объекта изменить другой поток?

Кроме того, я убежден, что кэши x-уровня отвечают за большинство невоспроизводимых ошибок времени выполнения. Это потому, что разработчики обычно не изучают низкоуровневые вещи, например, как работает процессор или как иерархия памяти влияет на работу приложений: http://en.wikipedia.org/wiki/Memory_hierarchy

Остается загадкой, почему классы программирования не начинаются с иерархии памяти и архитектуры процессора. "Привет мир" здесь не поможет.;)

Ответ 8

Когда вы вызываете notify() из объекта t, java уведомляет о конкретном методе t.wait(). Но как java ищет и уведомляет о конкретном методе ожидания.

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