Dlaczego trzeba czekać () zawsze musi być w synchronizowanym bloku


257

Wszyscy wiemy, że aby wywołać Object.wait(), to wywołanie musi być umieszczone w zsynchronizowanym bloku, w przeciwnym razie IllegalMonitorStateExceptionzostanie wyrzucone. Ale jaki jest powód wprowadzenia tego ograniczenia? Wiem, że to wait()zwalnia monitor, ale dlaczego musimy jawnie przejąć monitor, synchronizując określony blok, a następnie zwolnić monitor, dzwoniąc wait()?

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?

Odpowiedzi:


232

Ma wait()to sens tylko wtedy, gdy istnieje notify(), więc zawsze chodzi o komunikację między wątkami, a do prawidłowego działania potrzebna jest synchronizacja. Można argumentować, że powinno to być dorozumiane, ale tak naprawdę nie pomogłoby to z następującego powodu:

Semantycznie nigdy nie tylko wait(). Potrzebujesz pewnego warunku, by zostać usatysfakcjonowanym, a jeśli tak nie jest, poczekaj, aż to nastąpi. Więc tak naprawdę to robisz

if(!condition){
    wait();
}

Ale warunek jest ustawiany przez osobny wątek, więc aby poprawnie działać, potrzebujesz synchronizacji.

Jeszcze kilka rzeczy jest z tym nie tak, że fakt, że Twój wątek zakończył się, nie oznacza, że ​​warunek, którego szukasz, jest spełniony:

  • Możesz uzyskać fałszywe budzenie (co oznacza, że ​​wątek może obudzić się z oczekiwania bez otrzymywania powiadomienia) lub

  • Warunek można ustawić, ale trzeci wątek ponownie powoduje, że warunek ten jest fałszywy, gdy wątek oczekujący budzi się (i ponownie uzyskuje monitor).

Aby poradzić sobie z tymi przypadkami, tak naprawdę potrzebujesz zawsze jakiejś odmiany:

synchronized(lock){
    while(!condition){
        lock.wait();
    }
}

Jeszcze lepiej, nie zadzieraj z prymitywami synchronizacji i pracuj z abstrakcjami oferowanymi w java.util.concurrentpakietach.


3
Jest tu także szczegółowa dyskusja, mówiąc zasadniczo to samo. coding.derkeiler.com/Archive/Java/comp.lang.java.programmer/…

1
btw, jeśli nie chcesz ignorować flagi przerywanej, pętla również sprawdzi Thread.interrupted().
bestsss

2
Nadal mogę zrobić coś takiego: while (! Condition) {synchronized (this) {wait ();}}, co oznacza, że ​​nadal istnieje wyścig między sprawdzaniem warunku a oczekiwaniem, nawet jeśli funkcja wait () jest poprawnie wywoływana w bloku synchronicznym. Czy jest więc jakiś inny powód tego ograniczenia, być może ze względu na sposób jego implementacji w Javie?
shrini1000

9
Kolejny nieprzyjemny scenariusz: warunek jest fałszywy, mamy zamiar przejść do wait (), a następnie inny wątek zmienia warunek i wywołuje powiadomienie (). Ponieważ nie czekamy jeszcze (), przegapimy to powiadomienie (). Innymi słowy, testuj i czekaj, a także zmieniaj i powiadamiaj muszą być atomowe .

1
@Nullpointer: Jeśli jest to typ, który można zapisać atomowo (na przykład wartość logiczna wynikająca z użycia go bezpośrednio w klauzuli if), i nie ma żadnej współzależności z innymi udostępnionymi danymi, możesz uchylić się od uznania go za niestabilny. Ale potrzebujesz tego lub synchronizacji, aby zapewnić, że aktualizacja będzie widoczna dla innych wątków niezwłocznie.
Michael Borgwardt,

282

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ć:

  1. Wątek konsumenta wywołuje take()i widzi, że buffer.isEmpty().

  2. Zanim wątek konsumencki zadzwoni wait(), pojawia się wątek producenta i wywołuje pełny give(), czylibuffer.add(data); notify();

  3. Wątek konsumenta będzie teraz dzwonił wait()(i pomija ten, notify()który właśnie został wywołany).

  4. 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, synchronizedaby upewnić się, że notifynigdy nie jest wywoływane między isEmptya 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 synchronizedblokach.


Ten post został przepisany jako artykuł tutaj: Java: Dlaczego trzeba czekać w zsynchronizowanym bloku


Ponadto, aby upewnić się, że zmiany wprowadzone w warunku są widoczne natychmiast po zakończeniu funkcji wait (), tak myślę. W przeciwnym razie również zakleszczenie od momentu wywołania funkcji notyfikacji ().
Surya Wijaya Madjid

Ciekawe, ale zauważ, że samo zsynchronizowane wywoływanie nie zawsze rozwiązuje takie problemy ze względu na „niewiarygodny” charakter funkcji wait () i notify (). Przeczytaj więcej tutaj: stackoverflow.com/questions/21439355/… . Powód, dla którego potrzebna jest synchronizacja, leży w architekturze sprzętowej (patrz moja odpowiedź poniżej).
Marcus

ale jeśli dodajesz return buffer.remove();podczas blokowania, ale później wait();, to działa?
BobJiang

@ BobJiang, nie, wątek może zostać obudzony z powodów innych niż ktoś, kto dzwoni. Innymi słowy, bufor może być pusty nawet po waitzwrotach.
aioobe

Mam tylko Thread.currentThread().wait();w mainfunkcji otoczonej try-catch dla InterruptedException. Bez synchronizedbloku daje mi ten sam wyjątek IllegalMonitorStateException. Co sprawia, że ​​teraz osiąga stan nielegalny? Działa jednak wewnątrz synchronizedbloku.
Shashwat

12

@Rollerball ma rację. Wywoływane wait()jest, aby wątek mógł czekać na wystąpienie pewnego warunku, gdy to wait()wywołanie zostanie wykonane, wątek jest zmuszony zrezygnować z blokady.
Aby coś zrezygnować, musisz go najpierw posiadać. Wątek musi najpierw posiadać zamek. Stąd potrzeba wywołania go w synchronizedmetodzie / bloku.

Tak, zgadzam się ze wszystkimi powyższymi odpowiedziami dotyczącymi potencjalnych szkód / niespójności, jeśli nie sprawdziłeś warunku w synchronizedmetodzie / bloku. Jednak, jak zauważył @ shrini1000, samo wywołanie wait()w obrębie zsynchronizowanego bloku nie zapobiegnie tej niespójności.

Oto miła lektura ..


5
@Peyey Wyjaśnij „poprawnie” poprawnie. Twój komentarz nikomu się nie przyda.
Markiz Lorne

4

Problem, który może powodować, jeśli nie synchronizujesz wcześniej, wait()jest następujący:

  1. Jeśli pierwszy wątek wejdzie makeChangeOnX()i sprawdzi warunek while, i jest true( x.metCondition()wraca false, znaczy x.conditionjest false), więc wejdzie do niego. Następnie tuż przed wait()sposobu, inny wątek idzie setConditionToTrue()i ustawia x.conditionna truei notifyAll().
  2. Dopiero potem pierwszy wątek wejdzie w jego wait()metodę (bez wpływu na to, notifyAll()co wydarzyło się kilka chwil wcześniej). W takim przypadku pierwszy wątek będzie czekał na wykonanie kolejnego wątku setConditionToTrue(), ale może się to nie powtórzyć.

Ale jeśli umieścisz synchronizedprzed metodami zmieniającymi stan obiektu, tak się nie stanie.

class A {

    private Object X;

    makeChangeOnX(){
        while (! x.getCondition()){
            wait();
            }
        // Do the change
    }

    setConditionToTrue(){
        x.condition = true; 
        notifyAll();

    }
    setConditionToFalse(){
        x.condition = false;
        notifyAll();
    }
    bool getCondition(){
        return x.condition;
    }
}

2

Wszyscy wiemy, że do komunikacji między wątkami używane są metody wait (), notyfikuj () i notyfikuj wszystkie (). Aby pozbyć się brakującego sygnału i fałszywych problemów z budzeniem, oczekująca nić zawsze czeka na pewne warunki. na przykład-

boolean wasNotified = false;
while(!wasNotified) {
    wait();
}

Następnie powiadamiając zestawy wątków zmienna wasNotified na true i powiadamiaj.

Każdy wątek ma swoją lokalną pamięć podręczną, więc wszystkie zmiany najpierw są tam zapisywane, a następnie stopniowo przenoszone do pamięci głównej.

Gdyby te metody nie zostały wywołane w bloku zsynchronizowanym, zmienna wasNotified nie zostałaby wpuszczona do pamięci głównej i znajdowałaby się w lokalnej pamięci podręcznej wątku, więc wątek oczekujący będzie czekał na sygnał, chociaż został zresetowany przez powiadomienie wątku.

Aby rozwiązać tego rodzaju problemy, metody te są zawsze wywoływane w bloku synchronicznym, co zapewnia, że ​​po uruchomieniu bloku synchronicznego wszystko zostanie odczytane z pamięci głównej i zostanie opróżnione do pamięci głównej przed wyjściem z bloku synchronicznego.

synchronized(monitor) {
    boolean wasNotified = false;
    while(!wasNotified) {
        wait();
    }
}

Dzięki, mam nadzieję, że to wyjaśni.


1

Zasadniczo ma to związek z architekturą sprzętową (tj. RAM i pamięci podręczne ).

Jeśli nie używasz synchronizedrazem z wait()lub notify(), inny wątek może wejść do tego samego bloku, zamiast czekać na wejście monitora. Ponadto, np. Podczas uzyskiwania dostępu do tablicy bez zsynchronizowanego bloku, inny wątek może nie zobaczyć zmiany w nim ... w rzeczywistości inny wątek nie zobaczy żadnych zmian, jeśli ma już kopię tablicy w pamięci podręcznej poziomu x ( aka 1. / 2. / 3. poziom pamięci podręcznej) rdzenia procesora obsługującego wątki.

Ale zsynchronizowane bloki to tylko jedna strona medalu: jeśli faktycznie uzyskujesz dostęp do obiektu w zsynchronizowanym kontekście z niezsynchronizowanego kontekstu, obiekt nadal nie będzie synchronizowany nawet w obrębie zsynchronizowanego bloku, ponieważ zawiera własną kopię obiekt w pamięci podręcznej. Pisałem o tych problemach tutaj: https://stackoverflow.com/a/21462631 i kiedy blokada zawiera nie-końcowy obiekt, czy odwołanie do obiektu może być zmienione przez inny wątek?

Ponadto jestem przekonany, że pamięci podręczne poziomu X są odpowiedzialne za większość niepowtarzalnych błędów środowiska wykonawczego. Jest tak, ponieważ programiści zwykle nie uczą się rzeczy niskiego poziomu, takich jak działanie procesora lub jak hierarchia pamięci wpływa na działanie aplikacji: http://en.wikipedia.org/wiki/Memory_hierarchy

Pozostaje zagadką, dlaczego klasy programowania nie zaczynają się najpierw od hierarchii pamięci i architektury procesora. „Witaj świecie” tu nie pomoże. ;)


1
Właśnie odkryłem stronę internetową, która wyjaśnia to doskonale i dogłębnie: javamex.com/tutorials/...
Marcus

Hmm .. nie jestem pewien czy podążam. Jeśli buforowanie było jedynym powodem umieszczenia oczekiwania i powiadomienia wewnątrz zsynchronizowanego, dlaczego synchronizacja nie jest umieszczona w implementacji oczekiwania / powiadomienia?
aioobe

Dobre pytanie, skoro funkcja czekania / powiadamiania mogłaby równie dobrze zsynchronizować metody ... może dawni programiści Java firmy Sun znają odpowiedź? Spójrz na powyższy link, a może to również pomoże: docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
Marcus

Przyczyną może być: We wczesnych dniach Java nie wystąpiły błędy kompilacji, gdy nie wykonano synchronizacji przed wykonaniem tych operacji wielowątkowych. Zamiast tego wystąpiły tylko błędy w czasie wykonywania (np. Coderanch.com/t/239491/java-programmer-SCJP/certification/… ). Być może naprawdę myśleli @SUN, że kiedy programiści dostają te błędy, kontaktują się z nimi, co mogło dać im możliwość sprzedaży większej liczby serwerów. Kiedy to się zmieniło? Może Java 5.0 lub 6.0, ale tak naprawdę nie pamiętam, żeby być szczerym ...
Marcus

TBH Widzę kilka problemów z twoją odpowiedzią 1) Twoje drugie zdanie nie ma sensu: nie ma znaczenia, w którym obiekcie wątek ma blokadę. Niezależnie od tego, na jakim obiekcie synchronizowane są dwa wątki, wszystkie zmiany są widoczne. 2) Mówisz, że kolejny wątek „nie zobaczy żadnych zmian. Powinno to być „może nie” . 3) Nie wiem, dlaczego przywołujesz pamięć podręczną na poziomie 1/2/3 ... Ważne jest tutaj to, co mówi Model pamięci Java i które jest określone w JLS. Chociaż architektura sprzętowa może pomóc w zrozumieniu, dlaczego JLS mówi, co robi, w tym kontekście jest ściśle nieistotna.
aioobe,

0

bezpośrednio z tego samouczka java oracle:

Gdy wątek wywołuje d. Czekać, musi posiadać wewnętrzną blokadę dla d - w przeciwnym razie zostanie zgłoszony błąd. Wywołanie oczekiwania w metodzie zsynchronizowanej to prosty sposób na uzyskanie wewnętrznej blokady.


Z pytania zadanego przez autora nie wynika, że ​​autor pytania jasno rozumie, co cytowałem z samouczka, a ponadto moja odpowiedź wyjaśnia „dlaczego”.
Rollerball

0

Po wywołaniu funkcji powiadomienie () z obiektu t, java powiadamia określoną metodę t.wait (). Ale w jaki sposób Java wyszukuje i powiadamia określoną metodę oczekiwania.

java sprawdza tylko zsynchronizowany blok kodu, który został zablokowany przez obiekt t. java nie może przeszukiwać całego kodu w celu powiadomienia określonego t.wait ().


0

zgodnie z dokumentami:

Bieżący wątek musi być właścicielem monitora tego obiektu. Wątek zwalnia własność tego monitora.

wait()metoda oznacza po prostu zwolnienie blokady obiektu. Zatem obiekt zostanie zablokowany tylko w obrębie synchronizowanego bloku / metody. Jeśli wątek znajduje się poza blokiem synchronizacji, oznacza to, że nie jest zablokowany, jeśli nie jest zablokowany, to co byś zwolnił na obiekcie?


0

Oczekiwanie na wątek na obiekcie monitorującym (obiekcie używanym przez blok synchronizacji). Może istnieć n obiektów monitorujących w całej podróży jednego wątku. Jeśli Wątek czeka poza blokiem synchronizacji, wówczas nie ma obiektu monitorowania, a także inny wątek powiadamia o dostępie do obiektu monitorowania, więc skąd wątek poza blokiem synchronizacji wiedziałby, że został powiadomiony. Jest to również jeden z powodów, dla których czekaj (), powiadom () i notyfikuj () są w klasie obiektowej, a nie w klasie wątków.

Zasadniczo obiekt monitorowania jest tutaj wspólnym zasobem dla wszystkich wątków, a obiekty monitorowania mogą być dostępne tylko w bloku synchronizacji.

class A {
   int a = 0;
  //something......
  public void add() {
   synchronization(this) {
      //this is your monitoring object and thread has to wait to gain lock on **this**
       }
  }
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.