Який потенційний збиток, якщо можна було викликати 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: Чому чекати потрібно викликати в синхронізованому блоці