Почему wait () всегда должно быть в синхронизированном блоке - PullRequest
245 голосов
/ 06 мая 2010

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

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

Ответы [ 9 ]

265 голосов
/ 06 мая 2010

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

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

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


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

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

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


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

221 голосов
/ 06 мая 2010

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

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

if(!condition){
    wait();
}

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

Еще пара ошибок, связанных с этим, когда то, что ваш поток завершил ожидание, не означает, что условие, которое вы ищете, истинно:

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

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

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

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

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

11 голосов
/ 14 августа 2013

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

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

Вот хорошее чтение ..

4 голосов
/ 26 февраля 2016

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

  1. Если 1-й поток входит в makeChangeOnX() и проверяет условие while, и он равен true (x.metCondition() возвращает false, значит x.condition равен false), поэтому он попадет внутрь него. Затем, непосредственно перед методом wait(), другой поток переходит на setConditionToTrue() и устанавливает x.condition на true и notifyAll().
  2. Тогда только после этого 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;
    }
}
2 голосов
/ 01 декабря 2017

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

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

Затем уведомляющий поток устанавливает переменную wasNotified в true и уведомляет.

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

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

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

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

Спасибо, надеюсь, это прояснит.

0 голосов
/ 03 июня 2019

согласно документации:

Текущий поток должен владеть монитором этого объекта. Тема выпускает владение этим монитором.

wait() метод просто означает, что он снимает блокировку на объекте. Таким образом, объект будет заблокирован только внутри синхронизированного блока / метода. Если поток находится вне блока синхронизации, значит, он не заблокирован, если он не заблокирован, что бы вы освободили для объекта?

0 голосов
/ 10 марта 2016

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

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

0 голосов
/ 28 января 2014

Это в основном связано с аппаратной архитектурой (то есть RAM и кэши ).

Если вы не используете synchronized вместе с wait() или notify(), другой поток может войти в тот же блок, не дожидаясь, пока монитор введет его. Более того, когда, например, обращаясь к массиву без синхронизированного блока, другой поток может не увидеть изменения в нем ... фактически другой поток не будет видеть какие-либо изменения в нем , когда у него уже есть копия массив в кэше уровня x (он же кэш 1-го / 2-го / 3-го уровня) потока, обрабатывающего ядро ​​ЦП.

Но синхронизированные блоки - это только одна сторона медали: если вы фактически обращаетесь к объекту в синхронизированном контексте из несинхронизированного контекста, объект все равно не будет синхронизирован даже внутри синхронизированного блока, поскольку он содержит собственный копия объекта в его кеше. Я писал об этих проблемах здесь: https://stackoverflow.com/a/21462631 и Когда блокировка содержит неконечный объект, может ли ссылка на объект все еще быть изменена другим потоком?

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

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

0 голосов
/ 27 мая 2013

непосредственно из этого руководства по Java-оракулу:

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

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