Каков потенциальный ущерб, если можно было вызвать 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()
в результате того, что поток потребителя никогда не просыпается, и у нас есть тупик.
Как только вы поймете проблему, решение станет очевидным: используйте, synchronized
чтобы убедиться, что notify
никогда не вызывается между isEmpty
и wait
.
Не вдаваясь в детали: эта проблема синхронизации является универсальной. Как указывает Майкл Боргвардт, ожидание / уведомление - это все о связи между потоками, поэтому у вас всегда будет состояние гонки, подобное описанному выше. Вот почему применяется правило «только ждать внутри синхронизированного».
Абзац по ссылке, опубликованной @Willie, резюмирует это довольно хорошо:
Вам нужна абсолютная гарантия того, что официант и уведомитель договорились о состоянии предиката. Официант проверяет состояние предиката в некоторой точке ДО ТОГО, как он переходит в режим сна, но это зависит от правильности предиката, являющегося истинным, КОГДА он переходит в режим сна. Между этими двумя событиями существует период уязвимости, который может нарушить работу программы.
Предикат, с которым производитель и потребитель должны договориться, находится в приведенном выше примере buffer.isEmpty()
. Соглашение разрешается путем обеспечения того, что ожидание и уведомление выполняются synchronized
блоками.
Этот пост был переписан как статья здесь: Java: почему нужно вызывать wait в синхронизированном блоке