Jakie jest potencjalne uszkodzenie, gdyby można było wywołać wait()
poza zsynchronizowanym blokiem, zachowując jego semantykę - zawieszając wątek dzwoniącego?
Zilustrujmy problemy, na które natrafilibyśmy, gdybyśmy wait()
mogli je wywołać poza zsynchronizowanym blokiem, z konkretnym przykładem .
Załóżmy, że mamy zaimplementować kolejkę blokującą (wiem, że jest już jedna w API :)
Pierwsza próba (bez synchronizacji) może wyglądać mniej więcej tak jak poniżej
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();
}
}
Oto, co może się zdarzyć:
Wątek konsumenta wywołuje take()
i widzi, że buffer.isEmpty()
.
Zanim wątek konsumencki zadzwoni wait()
, pojawia się wątek producenta i wywołuje pełny give()
, czylibuffer.add(data); notify();
Wątek konsumenta będzie teraz dzwonił wait()
(i pomija ten, notify()
który właśnie został wywołany).
W przypadku pecha wątek producenta nie będzie produkował więcej, give()
ponieważ wątek konsumencki nigdy się nie budzi, a my mamy impas.
Po zrozumieniu problemu rozwiązanie jest oczywiste: użyj, synchronized
aby upewnić się, że notify
nigdy nie jest wywoływane między isEmpty
a wait
.
Bez wchodzenia w szczegóły: ten problem synchronizacji jest uniwersalny. Jak zauważa Michael Borgwardt, funkcja czekania / powiadamiania dotyczy komunikacji między wątkami, więc zawsze będziesz mieć wyścig podobny do opisanego powyżej. Właśnie dlatego obowiązuje zasada „tylko zsynchronizuj czekanie w środku”.
Akapit z linku opublikowanego przez @Willie całkiem dobrze to podsumowuje:
Potrzebujesz absolutnej gwarancji, że kelner i zgłaszający zgadzają się co do stanu orzeczenia. Kelner w pewnym momencie sprawdza stan orzeczenia PRZED pójściem spać, ale poprawność zależy od tego, czy orzeczenie jest prawdziwe KIEDY idzie spać. Między tymi dwoma zdarzeniami występuje okres podatności, który może uszkodzić program.
Predykat, który producent i konsument muszą uzgodnić, znajduje się w powyższym przykładzie buffer.isEmpty()
. Umowa zostaje rozwiązana poprzez upewnienie się, że oczekiwanie i powiadomienia są wykonywane w synchronized
blokach.
Ten post został przepisany jako artykuł tutaj: Java: Dlaczego trzeba czekać w zsynchronizowanym bloku