Вопрос-ответ

Why must wait() always be in synchronized block

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

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

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

Переведено автоматически
Ответ 1

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

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


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


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


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


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

Ответ 2

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

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

if(!condition){
wait();
}

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

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


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


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


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

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

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

Ответ 3

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

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

Вот приятное чтение..

Ответ 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;
}
}
java multithreading concurrency