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