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


257

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

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

Ответы:


232

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

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

if(!condition){
    wait();
}

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

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

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

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

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

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

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


3
Здесь также есть подробное обсуждение, говорящее по сути то же самое. coding.derkeiler.com/Archive/Java/comp.lang.java.programmer/...

1
Кстати, если вы не хотите игнорировать прерванный флаг, цикл также должен проверить Thread.interrupted().
bestsss

2
Я все еще могу сделать что-то вроде: while (! Condition) {synchronized (this) {wait ();}}, что означает, что между проверкой условия и ожиданием все еще есть гонка, даже если wait () правильно вызывается в синхронизированном блоке. Так есть ли какие-либо другие причины этого ограничения, возможно, из-за того, как оно реализовано в Java?
shrini1000

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

1
@Nullpointer: Если это тип, который может быть написан атомарно (например, логическое значение, подразумеваемое при использовании его непосредственно в предложении if), и нет никакой взаимозависимости с другими общими данными, вы можете избежать объявления его как volatile. Но вам нужно либо это, либо синхронизация, чтобы гарантировать, что обновление будет видно другим потокам сразу.
Майкл Боргвардт

283

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


Кроме того, чтобы убедиться, что изменения, внесенные в условие, видны сразу после завершения wait (), я думаю. Иначе, также тупик, так как notify () уже был вызван.
Сурия Виджая Маджид

Интересно, но обратите внимание, что просто вызовы synchronized не всегда решают такие проблемы из-за «ненадежной» природы wait () и notify (). Подробнее читайте здесь: stackoverflow.com/questions/21439355/… . Причина, по которой требуется синхронизация, лежит в аппаратной архитектуре (см. Мой ответ ниже).
Маркус

но если добавить return buffer.remove();в то время как блок, но после wait();, это работает?
BobJiang

@BobJiang, нет, нить можно разбудить по причинам, отличным от того, кто звонит. Другими словами, буфер может быть пустым даже после waitвозврата.
aioobe

У меня только Thread.currentThread().wait();в mainфункции, окруженной try-catch для InterruptedException. Без synchronizedблока это дает мне то же исключение IllegalMonitorStateException. Что заставляет это достигнуть незаконного государства теперь? Это работает внутри synchronizedблока, хотя.
Шашват

12

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

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

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


5
@Popeye Объясните «правильно» правильно. Ваш комментарий никому не нужен.
Маркиз Лорн

4

Проблема, которая может возникнуть, если вы не выполняете синхронизацию, 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

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

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

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

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

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

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

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

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


1

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

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

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

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

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


1
Только что обнаружил веб-сайт, который объясняет это прекрасно и подробно: javamex.com/tutorials/…
Маркус

Хм .. не уверен, что я следую. Если кеширование было единственной причиной для размещения ожидания и уведомления внутри синхронизированного, то почему синхронизация не помещается в реализацию ожидания / уведомления?
aioobe

Хороший вопрос, поскольку wait / notify вполне могут быть синхронизированными методами ... может быть, бывшие Java-разработчики Sun знают ответ? Посмотрите на ссылку выше, или, может быть, это также поможет вам: docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
Маркус

Причина может заключаться в том, что в первые дни Java не было ошибок компиляции, когда не выполнялись вызовы синхронизации перед выполнением этих операций многопоточности. Вместо этого были только ошибки времени выполнения (например, coderanch.com/t/239491/java-programmer-SCJP/certification/… ). Возможно, они действительно подумали @SUN, что когда программисты получают эти ошибки, с ними связываются, что, возможно, дало им возможность продать больше своих серверов. Когда это изменилось? Может быть, Java 5.0 или 6.0, но на самом деле я не помню, чтобы быть честным ...
Маркус

TBH Я вижу несколько проблем с вашим ответом 1) Ваше второе предложение не имеет смысла: не имеет значения, на каком объекте у потока есть блокировка. Независимо от того, на каком объекте синхронизируются два потока, все изменения становятся видимыми. 2) Вы говорите, что другой поток "не увидит" никаких изменений. Это должно быть "не может" . 3) Я не знаю, почему вы используете кэш 1-го / 2-го / 3-го уровня ... Здесь важно то, что говорит модель памяти Java и что указано в JLS. Хотя аппаратная архитектура может помочь понять, почему JLS говорит, что она делает, в этом контексте она, строго говоря, не имеет значения.
aioobe

0

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

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


Из вопроса, сделанного автором, не видно, что у автора вопроса есть четкое понимание того, что я цитировал из учебника. И, кроме того, мой ответ объясняет «почему».
Роллербол

0

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

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


0

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

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

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


0

Ожидание потока на объекте мониторинга (объект, используемый блоком синхронизации). Может быть n номеров объекта мониторинга во всем пути одного потока. Если поток ожидает за пределами блока синхронизации, тогда объект мониторинга отсутствует, а также другой поток уведомляет о доступе к объекту мониторинга, так как бы поток вне блока синхронизации мог узнать, что он был уведомлен. Это также одна из причин того, что wait (), notify () и notifyAll () находятся в классе объекта, а не в классе потока.

По сути, объект мониторинга является общим ресурсом для всех потоков, а объекты мониторинга могут быть доступны только в блоке синхронизации.

class A {
   int a = 0;
  //something......
  public void add() {
   synchronization(this) {
      //this is your monitoring object and thread has to wait to gain lock on **this**
       }
  }
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.