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