Java: powiadomienie () vs. powiadomienie ponownie () od nowa


377

Jeśli jeden Googles dla „różnicy między notify()i notifyAll()”, pojawi się wiele wyjaśnień (pomijając akapity javadoc). To wszystko sprowadza się do liczby wątków oczekujących jest obudzony: jeden w notify()i wszystkie in notifyAll().

Jednak (jeśli dobrze rozumiem różnicę między tymi metodami), tylko jeden wątek jest zawsze wybierany do dalszego monitorowania monitora; w pierwszym przypadku ten wybrany przez maszynę wirtualną, w drugim przypadku ten wybrany przez systemowy program do planowania wątków. Dokładne procedury wyboru dla obu z nich (w ogólnym przypadku) nie są znane programiście.

Jaka jest zatem użyteczna różnica między powiadomieniem () a powiadomieniemAll () ? Czy coś brakuje?


6
Biblioteki przydatne do współbieżności znajdują się w bibliotekach współbieżności. Sugeruję, że są to lepszy wybór w prawie każdym przypadku. Biblioteka Concurency sprzed Java 5.0 (w której zostały dodane jako standard w 2004 r.)
Peter Lawrey

4
Nie zgadzam się z Peterem. Biblioteka współbieżności jest zaimplementowana w Javie i za każdym razem, gdy wywołujesz blokadę (), odblokowanie () itp., Wykonuje się dużo kodu Java. Możesz strzelić sobie w stopę, używając biblioteki współbieżności zamiast starego synchronized, dobrego , z wyjątkiem niektórych , raczej rzadkie przypadki użycia.
Alexander Ryzhov,

2
Kluczowe nieporozumienie wydaje się następujące: ... tylko jeden wątek jest zawsze wybierany do dalszego monitorowania monitora; w pierwszym przypadku ten wybrany przez maszynę wirtualną, w drugim przypadku ten wybrany przez systemowy program do planowania wątków. Oznacza to, że są one zasadniczo takie same. Chociaż opisane zachowanie jest prawidłowe, brakuje w tym notifyAll()przypadku: pozostałe wątki po pierwszym pozostają w stanie czuwania i przejmują monitor, jeden po drugim. W tym notifyprzypadku żaden z pozostałych wątków nie został nawet obudzony. Więc funkcjonalnie są bardzo różne!
BeeOnRope

1) Jeśli na wątek czeka wiele wątków, a powiadomienie () jest wywoływane tylko raz na tym obiekcie. Z wyjątkiem jednego z oczekujących wątków pozostałe wątki czekają wiecznie? 2) Jeśli zostanie użyte powiadomienie (), rozpocznie się wykonywanie tylko jednego z wielu oczekujących wątków. Jeśli zostanie użyte powiadomienieall (), wszystkie oczekujące wątki zostaną powiadomione, ale tylko jeden z nich zacznie się uruchamiać, więc do czego służy tutaj powiadomienie?
Chetan Gowda

@ChetanGowda Powiadamianie wszystkich wątków vs Powiadom tylko dokładnie jeden arbitralny wątek ma istotną różnicę, dopóki ta pozornie subtelna, ale ważna różnica nie dotknie nas. Gdy powiadomisz () tylko 1 wątek, wszystkie pozostałe wątki będą w stanie oczekiwania, dopóki nie otrzyma wyraźnego powiadomienia /sygnał. Powiadamiając o wszystkich, wszystkie wątki będą wykonywane i uzupełniane w określonej kolejności, jeden po drugim, bez dalszego powiadamiania - tutaj powinniśmy powiedzieć, że wątki są, blockeda nie. waitingGdy blockedjego exec zostanie tymczasowo zawieszone, aż inny wątek znajdzie się w syncbloku.
user104309,

Odpowiedzi:


248

Jednak (jeśli dobrze rozumiem różnicę między tymi metodami), tylko jeden wątek jest zawsze wybierany do dalszego monitorowania monitora.

To nie jest poprawne. o.notifyAll()budzi wszystkie wątki zablokowane w o.wait()połączeniach. Nitki są dozwolone tylko do powrotu z o.wait()jeden po drugim, ale każdy z nich będzie dostać swoją kolej.


Mówiąc najprościej, zależy to od tego, dlaczego twoje wątki czekają na powiadomienie. Czy chcesz powiedzieć jednemu z oczekujących wątków, że coś się wydarzyło, czy też chcesz powiedzieć wszystkim jednocześnie?

W niektórych przypadkach wszystkie wątki oczekujące mogą podjąć przydatne działania po zakończeniu oczekiwania. Przykładem może być zestaw wątków oczekujących na zakończenie określonego zadania; po zakończeniu zadania wszystkie oczekujące wątki mogą kontynuować swoją działalność. W takim przypadku użyłbyś funkcji replaceAll (), aby obudzić wszystkie oczekujące wątki w tym samym czasie.

W innym przypadku, na przykład blokując się wzajemnie, tylko jeden z oczekujących wątków może zrobić coś użytecznego po otrzymaniu powiadomienia (w tym przypadku uzyskać blokadę). W takim przypadku wolisz użyć funkcji powiadomień () . Prawidłowo wdrożone, to mógłby użyć notifyAll () w tej sytuacji, jak również, ale byś niepotrzebnie budzić wątki, które nie mogą nic zrobić tak.


W wielu przypadkach kod oczekiwania na warunek zostanie zapisany jako pętla:

synchronized(o) {
    while (! IsConditionTrue()) {
        o.wait();
    }
    DoSomethingThatOnlyMakesSenseWhenConditionIsTrue_and_MaybeMakeConditionFalseAgain();
}

W ten sposób, jeśli o.notifyAll()połączenie budzi więcej niż jeden wątek oczekujący, a pierwszy, który powróci z o.wait()make, pozostawia warunek w stanie fałszywym, wówczas pozostałe wątki, które zostały przebudzone, wrócą do oczekiwania.


29
jeśli powiadomisz tylko jeden wątek, ale wiele obiektów czeka na obiekt, jak maszyna wirtualna określa, który z nich powiadomić?
amfibia

6
Nie mogę powiedzieć z całą pewnością o specyfikacji Java, ale ogólnie powinieneś unikać przyjmowania założeń na temat takich szczegółów. Myślę, że możesz założyć, że VM zrobi to w rozsądny i przeważnie sprawiedliwy sposób.
Liedman,

15
Liedman poważnie się myli, specyfikacja Java wyraźnie stwierdza, że ​​notify () nie ma gwarancji, że jest uczciwy. tzn. każde wywołanie powiadomienia może ponownie obudzić ten sam wątek (kolejka wątków na monitorze NIE JEST FAIR lub FIFO). Jednak harmonogram ma gwarancję uczciwości. Dlatego w większości przypadków, w których masz więcej niż 2 wątki, powinieneś powiadomić powiadomienie Wszystkie.
Yann TM

45
@YannTM Jestem za konstruktywną krytyką, ale myślę, że twój ton jest nieco niesprawiedliwy. Powiedziałem wyraźnie „nie mogę powiedzieć na pewno” i „myślę”. Spokojnie, czy kiedykolwiek napisałeś coś siedem lat temu, co nie było w 100% poprawne?
Liedman

10
Problem polega na tym, że jest to zaakceptowana odpowiedź, nie jest to kwestia osobistej dumy. Jeśli wiesz, że teraz się myliłeś, edytuj swoją odpowiedź, aby to powiedzieć, i wskaż np. Pedagogiczną xagyg i poprawną odpowiedź poniżej.
Yann TM

330

Oczywiście, notifybudzi (dowolny) jeden wątek w zestawie oczekiwania, notifyAllbudzi wszystkie wątki w zestawie oczekiwania. Poniższa dyskusja powinna wyjaśnić wszelkie wątpliwości. notifyAllpowinien być używany przez większość czasu. Jeśli nie masz pewności, którego użyć, skorzystaj z. notifyAllPrzeczytaj wyjaśnienie poniżej.

Przeczytaj bardzo uważnie i zrozum. Proszę wysłać mi e-mail, jeśli masz jakieś pytania.

Spójrz na producenta / konsumenta (założenie jest klasą ProducerConsumer z dwiema metodami). JEST ZŁAMANY (ponieważ używa notify) - tak, MOŻE działać - nawet przez większość czasu, ale może również powodować impas - zobaczymy, dlaczego:

public synchronized void put(Object o) {
    while (buf.size()==MAX_SIZE) {
        wait(); // called if the buffer is full (try/catch removed for brevity)
    }
    buf.add(o);
    notify(); // called in case there are any getters or putters waiting
}

public synchronized Object get() {
    // Y: this is where C2 tries to acquire the lock (i.e. at the beginning of the method)
    while (buf.size()==0) {
        wait(); // called if the buffer is empty (try/catch removed for brevity)
        // X: this is where C1 tries to re-acquire the lock (see below)
    }
    Object o = buf.remove(0);
    notify(); // called if there are any getters or putters waiting
    return o;
}

PO PIERWSZE,

Dlaczego potrzebujemy pętli while otaczającej oczekiwanie?

Potrzebujemy whilepętli na wypadek wystąpienia takiej sytuacji:

Konsument 1 (C1) wchodzi do zsynchronizowanego bloku, a bufor jest pusty, więc C1 jest umieszczany w zestawie oczekiwania (za pośrednictwem waitpołączenia). Konsument 2 (C2) wkrótce wejdzie do metody zsynchronizowanej (w punkcie Y powyżej), ale producent P1 umieszcza obiekt w buforze, a następnie wywołuje notify. Jedynym oczekującym wątkiem jest C1, więc jest on budzony i teraz próbuje ponownie uzyskać blokadę obiektu w punkcie X (powyżej).

Teraz C1 i C2 próbują uzyskać blokadę synchronizacji. Jeden z nich (niedeterministycznie) jest wybierany i wchodzi do metody, drugi jest blokowany (nie czekając - ale blokowany, próbując uzyskać blokadę metody). Powiedzmy, że C2 najpierw otrzymuje blokadę. C1 nadal blokuje (próbuje zdobyć zamek w X). C2 kończy metodę i zwalnia blokadę. Teraz C1 przejmuje blokadę. Zgadnij, na szczęście mamy whilepętlę, ponieważ C1 wykonuje kontrolę pętli (warta) i nie można usunąć nieistniejącego elementu z bufora (C2 już go dostał!). Gdybyśmy nie mieli while, otrzymalibyśmy, IndexArrayOutOfBoundsExceptionponieważ C1 próbuje usunąć pierwszy element z bufora!

TERAZ,

Ok, teraz dlaczego potrzebujemy powiadomić?

W powyższym przykładzie producent / konsument wygląda na to, że możemy uciec notify. Wydaje się, że tak jest, ponieważ możemy udowodnić, że strażnicy w pętlach oczekiwania dla producenta i konsumenta wykluczają się wzajemnie. To znaczy, wygląda na to, że nie możemy mieć wątku oczekującego zarówno w putmetodzie, jak i getmetodzie, ponieważ aby to było prawdą, musiałyby być spełnione następujące warunki:

buf.size() == 0 AND buf.size() == MAX_SIZE (zakładamy, że MAX_SIZE nie jest równy 0)

JEDNAK nie jest to wystarczające, NALEŻY użyć notifyAll. Zobaczmy, dlaczego ...

Załóżmy, że mamy bufor wielkości 1 (aby ułatwić naśladowanie przykładu). Poniższe kroki prowadzą nas do impasu. Zauważ, że W KAŻDYM momencie wątek budzi się z powiadomieniem, może on zostać niedeterministycznie wybrany przez JVM - to znaczy każdy oczekujący wątek może zostać obudzony. Zauważ również, że gdy wiele wątków blokuje wejście do metody (tj. Próbuje uzyskać blokadę), kolejność pozyskiwania może być niedeterministyczna. Pamiętaj również, że wątek może być jednocześnie w jednej z metod - metody zsynchronizowane pozwalają na wykonywanie tylko jednego wątku (tj. Blokowanie) dowolnej (zsynchronizowanej) metody w klasie. Jeśli wystąpi następująca sekwencja zdarzeń - zakleszczenie:

KROK 1:
- P1 umieszcza 1 bufor w buforze

KROK 2:
- P2 próbuje put- sprawdza pętlę oczekiwania - już znak - czeka

KROK 3:
- P3 próbuje put- sprawdza pętlę oczekiwania - już znak - czeka

KROK 4:
- C1 próbuje uzyskać 1 znak
- C2 próbuje uzyskać 1 znak - bloki przy wejściu do getmetody
- C3 próbuje uzyskać 1 znak - bloki przy wejściu do getmetody

KROK 5:
- C1 jest wykonanie getmetody - dostaje char, rozmowy notifymetoda, wyjścia
- The notifyobudzi P2
- ALE, C2 wchodzi metodę przed P2 może (P2 musi odkupić zamek), więc bloków P2 na wejściu do putmetody
- C2 sprawdza pętlę oczekiwania, nie ma już znaków w buforze, więc czeka
- C3 wchodzi do metody po C2, ale przed P2, sprawdza pętlę oczekiwania, nie ma więcej znaków w buforze, więc czeka

KROK 6:
- TERAZ: czekają P3, C2 i C3!
- W końcu P2 przejmuje blokadę, umieszcza znak w buforze, wywołuje powiadomienia, wychodzi z metody

KROK 7:
- Powiadomienie P2 budzi P3 (pamiętaj, że każdy wątek może zostać obudzony)
- P3 sprawdza warunek pętli oczekiwania, w buforze jest już znak, więc czeka.
- BRAK WIĘCEJ NICI, ABY ZADZWOŃĆ DO POWIADOMIENIA, I TRZY NICI TRWAŁO ZAWIESZONE!

Rozwiązanie: Wymień notifysię notifyAllw kodzie producent / konsumentów (powyżej).


1
finnw - P3 musi ponownie sprawdzić warunek, ponieważ notifypowoduje, że P3 (wybrany wątek w tym przykładzie) przechodzi od punktu, w którym czekał (tj. w whilepętli). Istnieją inne przykłady, które nie powodują zakleszczenia, jednak w tym przypadku użycie notifynie gwarantuje kodu bez zakleszczenia. Zastosowanie notifyAllrobi.
xagyg

4
@marcus Bardzo blisko. Dzięki powiadomieniu wszystkie wątki ponownie uzyskają blokadę (pojedynczo), ale pamiętaj, że po ponownym uzyskaniu blokady przez jeden wątek i wykonaniu metody (a następnie wyjściu) ... następny wątek ponownie uzyskuje blokadę, sprawdza „while” i wróci do „czekania” (oczywiście w zależności od tego, jaki jest warunek). Tak więc powiadomienie budzi jeden wątek - jak poprawnie podajesz. powiadomienie Wszystkie budzi wszystkie wątki i każdy wątek przejmuje blokadę pojedynczo - sprawdza stan „while” i albo wykonuje metodę, albo „czeka” ponownie.
xagyg

1
@xagyg, czy mówisz o scenariuszu, w którym każdy producent ma do przechowywania tylko jeden znak? Jeśli tak, twój przykład jest poprawny, ale niezbyt interesujący IMO. Za pomocą dodatkowych kroków, które sugeruję, możesz zakleszczyć ten sam system, ale z nieograniczoną ilością danych wejściowych - w taki sposób zwykle stosuje się takie wzorce w rzeczywistości.
eran

3
@codeObserver Zapytałeś: "Wywołanie powiadomieniaAll () doprowadziłoby do tego, że wiele wątków oczekujących sprawdzałoby warunek while () w tym samym czasie .. i dlatego istnieje możliwość, że przed chwilą wygaszenia, 2 wątki są już z niego wyłączone, powodując outOfBound wyjątek? ” Nie, nie jest to możliwe, ponieważ chociaż obudziłoby się wiele wątków, nie mogą one jednocześnie sprawdzać warunku „while”. Każdy z nich musi ponownie uzyskać blokadę (natychmiast po odczekaniu), zanim ponownie wejdzie do sekcji kodu i ponownie sprawdzi czas. Dlatego pojedynczo.
xagyg,

4
@xagyg fajny przykład. To nie wchodzi w zakres pierwotnego pytania; tylko ze względu na dyskusję. Impas jest problemem projektowym imo (popraw mnie, jeśli się mylę). Ponieważ masz jedną blokadę wspólną dla obu opcji put i get. A JVM nie jest wystarczająco sprytny, aby zadzwonić po zwolnieniu blokady i vice versa. Blokada martwa zdarza się, ponieważ put budzi inne put, które wróciło do wait () z powodu while (). Czy zadziałałyby dwie klasy (i dwie blokady)? Więc wstaw {synchonized (get)}, get {(synchonized (put)}. Innymi słowy, get obudzi tylko put, a put budzi tylko get
Jay

43

Przydatne różnice:

  • Użyj powiadomienia (), jeśli wszystkie oczekujące wątki są wymienne (kolejność ich budzenia nie ma znaczenia) lub jeśli masz tylko jeden wątek oczekujący. Typowym przykładem jest pula wątków używana do wykonywania zadań z kolejki - po dodaniu zadania jeden z wątków jest powiadamiany o przebudzeniu, wykonaniu następnego zadania i przejściu w tryb uśpienia.

  • Użyj powiadomieniaAll () w innych przypadkach, gdy oczekujące wątki mogą mieć różne cele i powinny być w stanie działać jednocześnie. Przykładem jest operacja konserwacji współdzielonego zasobu, w której wiele wątków czeka na zakończenie operacji przed uzyskaniem dostępu do zasobu.


19

Myślę, że to zależy od tego, jak zasoby są wytwarzane i konsumowane. Jeśli dostępnych jest 5 obiektów roboczych naraz, a masz 5 obiektów konsumenckich, warto obudzić wszystkie wątki za pomocą funkcji powiadomieńAll (), aby każdy mógł przetworzyć 1 obiekt roboczy.

Jeśli masz tylko jeden obiekt roboczy, to po co budzić wszystkie obiekty konsumenckie, aby ścigać się o ten jeden obiekt? Pierwszy sprawdzi, czy dostępna praca jest dostępna, a wszystkie pozostałe wątki sprawdzą i stwierdzą, że nie mają nic do roboty.

Znalazłem tutaj świetne wytłumaczenie . W skrócie:

Metoda powiadomienie () jest zwykle używana w przypadku pul zasobów , gdzie istnieje dowolna liczba „konsumentów” lub „pracowników”, którzy pobierają zasoby, ale gdy zasób jest dodawany do puli, tylko jeden z oczekujących konsumentów lub pracowników może sobie poradzić z tym. Metoda powiadomienie () jest faktycznie używana w większości innych przypadków. Ściśle wymagane jest powiadomienie kelnerów o stanie, który może umożliwić kontynuowanie pracy wielu kelnerów. Ale często jest to trudne do poznania. Tak więc, ogólną zasadą jest, że jeśli nie masz konkretnej logiki do korzystania z replace () , to prawdopodobnie powinieneś użyć noticeAll () , ponieważ często trudno jest dokładnie wiedzieć, jakie wątki będą oczekiwać na danym obiekcie i dlaczego.


11

Zauważ, że dzięki narzędziom do współbieżności masz również wybór pomiędzy, signal()a signalAll()ponieważ metody te są tam nazywane. Tak więc pytanie pozostaje ważne nawet z java.util.concurrent.

Doug Lea porusza ciekawy punkt w swojej słynnej książce : jeśli notify()a Thread.interrupt()zdarzy się w tym samym czasie, powiadomienie może się zgubić. Jeśli może się to zdarzyć i ma to dramatyczne implikacje, notifyAll()jest bezpieczniejszym wyborem, nawet jeśli płacisz za koszty ogólne (budzenie zbyt wielu wątków przez większość czasu).


10

Krótkie podsumowanie:

Zawsze preferuj powiadomienieAll () niż powiadomienie (), chyba że masz masowo równoległą aplikację, w której duża liczba wątków robi to samo.

Wyjaśnienie:

powiadom () [...] budzi pojedynczy wątek. Ponieważ powiadomienie () nie pozwala na określenie wątku, który się budzi, jest użyteczny tylko w aplikacjach masowo równoległych - to znaczy w programach z dużą liczbą wątków, wszystkie wykonujące podobne zadania. W takiej aplikacji nie obchodzi Cię, który wątek się obudzi.

źródło: https://docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html

Porównaj powiadomienie () z powiadomieniem () w opisanej powyżej sytuacji: aplikacja masowo równoległa, w której wątki robią to samo. Jeśli zadzwonisz notifyAll () w tym przypadku notifyAll () będzie wywoływać budzi się (tj harmonogramu) z ogromnej liczby wątków, wiele z nich niepotrzebnie (bo tylko jeden wątek może właściwie postępować, a mianowicie wątek, który zostanie przyznano monitoruje obiekt , który został wywołany wait () , powiadomienie () lub powiadomienieAll () ), marnując zasoby komputerowe.

Tak więc, jeśli nie masz aplikacji, w której ogromna liczba wątków robi to samo jednocześnie, wolisz powiadomićAll () niż powiadomić () . Dlaczego? Ponieważ, jak inni użytkownicy już odpowiedzieli na tym forum, powiadom ()

budzi pojedynczy wątek, który czeka na monitorze tego obiektu. [...] Wybór jest arbitralny i zależy od uznania wdrożenia.

źródło: Java SE8 API ( https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#notify-- )

Wyobraź sobie, że masz aplikację konsumenta producenta, w której konsumenci są gotowi (tj. Czekać () ing) do konsumpcji, producenci są gotowi (tj. Czekać () ing) do produkcji, a kolejka produktów (do wyprodukowania / konsumpcji) jest pusta. W takim przypadku funkcja powiadomienie () może obudzić tylko konsumentów, a nigdy producentów, ponieważ wybór, kto się obudzi, jest arbitralny . Cykl konsumencki producenta nie zrobiłby żadnego postępu, chociaż producenci i konsumenci są gotowi odpowiednio do produkcji i konsumpcji. Zamiast tego budzi się konsument (tzn. Pozostawia status wait () ), nie usuwa elementu z kolejki, ponieważ jest pusty, i powiadamia innego konsumenta, aby kontynuować.

W przeciwieństwie do tego, replaceAll () budzi zarówno producentów, jak i konsumentów. Wybór harmonogramu zależy od harmonogramu. Oczywiście, w zależności od implementacji programu planującego, program planujący może również planować tylko konsumentów (np. Jeśli przypisujesz wątki konsumenckie o bardzo wysokim priorytecie). Założono tutaj jednak, że niebezpieczeństwo, że harmonogram szereguje tylko konsumentów, jest mniejsze niż niebezpieczeństwo, że JVM budzi tylko konsumentów, ponieważ jakikolwiek rozsądnie wdrożony harmonogram nie podejmuje tylko arbitralnych decyzji. Przeciwnie, większość implementacji harmonogramu podejmuje przynajmniej pewne wysiłki, aby zapobiec głodowi.


9

Oto przykład. Uruchom. Następnie zmień jedną z funkcji powiadomieńAll () na powiadomienie () i zobacz, co się stanie.

ProducerConsumerExample klasa

public class ProducerConsumerExample {

    private static boolean Even = true;
    private static boolean Odd = false;

    public static void main(String[] args) {
        Dropbox dropbox = new Dropbox();
        (new Thread(new Consumer(Even, dropbox))).start();
        (new Thread(new Consumer(Odd, dropbox))).start();
        (new Thread(new Producer(dropbox))).start();
    }
}

Klasa Dropbox

public class Dropbox {

    private int number;
    private boolean empty = true;
    private boolean evenNumber = false;

    public synchronized int take(final boolean even) {
        while (empty || evenNumber != even) {
            try {
                System.out.format("%s is waiting ... %n", even ? "Even" : "Odd");
                wait();
            } catch (InterruptedException e) { }
        }
        System.out.format("%s took %d.%n", even ? "Even" : "Odd", number);
        empty = true;
        notifyAll();

        return number;
    }

    public synchronized void put(int number) {
        while (!empty) {
            try {
                System.out.println("Producer is waiting ...");
                wait();
            } catch (InterruptedException e) { }
        }
        this.number = number;
        evenNumber = number % 2 == 0;
        System.out.format("Producer put %d.%n", number);
        empty = false;
        notifyAll();
    }
}

Klasa konsumencka

import java.util.Random;

public class Consumer implements Runnable {

    private final Dropbox dropbox;
    private final boolean even;

    public Consumer(boolean even, Dropbox dropbox) {
        this.even = even;
        this.dropbox = dropbox;
    }

    public void run() {
        Random random = new Random();
        while (true) {
            dropbox.take(even);
            try {
                Thread.sleep(random.nextInt(100));
            } catch (InterruptedException e) { }
        }
    }
}

Klasa producenta

import java.util.Random;

public class Producer implements Runnable {

    private Dropbox dropbox;

    public Producer(Dropbox dropbox) {
        this.dropbox = dropbox;
    }

    public void run() {
        Random random = new Random();
        while (true) {
            int number = random.nextInt(10);
            try {
                Thread.sleep(random.nextInt(100));
                dropbox.put(number);
            } catch (InterruptedException e) { }
        }
    }
}

8

Joshua Bloch, sam Java Guru w Effective Java 2nd edition:

„Pozycja 69: Preferuj narzędzia współbieżności, aby czekać i powiadamiać”.


16
Dlaczego jest ważniejsza niż źródła.
Pacerier,

2
@Pacerier Dobrze powiedziane. Byłbym bardziej zainteresowany ustaleniem przyczyn. Jednym z możliwych powodów może być to, że oczekiwanie i powiadamianie w klasie obiektów oparte jest na niejawnej zmiennej warunkowej. Tak więc w standardowym przykładzie producenta i konsumenta ... zarówno producent, jak i konsument będą czekać na ten sam warunek, co może doprowadzić do impasu, jak wyjaśnił Xagyg w swojej odpowiedzi. Więc lepszym rozwiązaniem jest użycie 2 zmiennych warunkach jak wyjaśniono w docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/...
rahul

6

Mam nadzieję, że rozwiąże to pewne wątpliwości.

replace () : Metoda powiadomienie () budzi jeden wątek oczekujący na blokadę (pierwszy wątek, który wywołał wait () na tej blokadzie).

replaceAll () : Metoda replaceAll () budzi wszystkie wątki oczekujące na blokadę; JVM wybiera jeden z wątków z listy wątków oczekujących na blokadę i budzi ten wątek.

W przypadku pojedynczego wątku oczekującego na blokadę, nie ma znaczącej różnicy między powiadomieniem () a powiadomieniemAll (). Jednakże, gdy na blokadę czeka więcej niż jeden wątek, zarówno w powiadomieniu (), jak i powiadomieniuAll (), dokładny przebudzony wątek znajduje się pod kontrolą JVM i nie można programowo kontrolować przebudzenia określonego wątku.

Na pierwszy rzut oka wydaje się, że dobrym pomysłem jest wywołanie funkcji powiadomień () w celu wybudzenia jednego wątku; obudzenie wszystkich wątków może wydawać się niepotrzebne. Jednak problem z powiadomieniem () polega na tym, że wątek obudzony może nie być odpowiedni do przebudzenia (wątek może czekać na inny warunek lub warunek nadal nie jest spełniony dla tego wątku itp.). W takim przypadku powiadomienie () może zostać utracone i żaden inny wątek nie obudzi się potencjalnie prowadząc do pewnego rodzaju zakleszczenia (powiadomienie zostanie utracone, a wszystkie inne wątki będą czekać na powiadomienie - na zawsze).

Aby uniknąć tego problemu , zawsze lepiej jest wywołać powiadomienieAll (), gdy na blokadę czeka więcej niż jeden wątek (lub więcej niż jeden warunek, w którym oczekiwanie jest wykonywane). Metoda replaceAll () budzi wszystkie wątki, więc nie jest bardzo wydajna. ta utrata wydajności jest jednak nieistotna w rzeczywistych zastosowaniach.


6

Istnieją trzy stany dla wątku.

  1. CZEKAJ - Wątek nie używa żadnego cyklu procesora
  2. ZABLOKOWANY - Wątek jest blokowany podczas próby uzyskania monitora. Być może nadal używa cykli procesora
  3. RUNNING - Wątek jest uruchomiony.

Teraz, gdy wywoływane jest powiadomienie (), JVM wybiera jeden wątek i przenosi go do stanu ZABLOKOWANEGO, a zatem do stanu URUCHOMIENIE, ponieważ obiekt monitorujący nie konkuruje.

Po wywołaniu funkcji powiadomieńAll () JVM wybiera wszystkie wątki i przenosi je do stanu ZABLOKOWANEGO. Wszystkie te wątki otrzymają blokadę obiektu na zasadzie pierwszeństwa. Wątek, który może najpierw przejąć monitor, będzie mógł najpierw przejść do stanu RUNNING i tak dalej.


Po prostu niesamowite wyjaśnienie.
royatirek

5

Jestem bardzo zaskoczony, że nikt nie wspominał o niesławnym problemie „zagubionego budzenia” (google it).

Gruntownie:

  1. jeśli masz wiele wątków oczekujących na ten sam warunek i
  2. wiele wątków, które mogą spowodować przejście ze stanu A do stanu B i
  3. wiele wątków, które mogą sprawić, że przejdziesz ze stanu B do stanu A (zwykle takie same wątki jak w 1.) i,
  4. przejście ze stanu A do B powinno powiadomić wątki w 1.

Następnie powinieneś skorzystać z funkcji powiadomień, chyba że masz udowodnione gwarancje, że utracone pobudki są niemożliwe.

Typowym przykładem jest współbieżna kolejka FIFO, w której: wiele modułów kolejkujących (1. i 3. powyżej) może przenieść kolejkę z pustej na niepustą. Wiele kolejek kolejkowych (2. powyżej) może czekać na warunek „kolejka nie jest pusta” pusta -> niepuste powinny powiadomić osoby odpowietrzające

Możesz łatwo napisać przeplot operacji, w którym, zaczynając od pustej kolejki, 2 kolejki kolejkowe i 2 kolejki kolejkowe współdziałają, a jedna kolejka kolejowa pozostanie w stanie uśpienia.

Jest to problem prawdopodobnie porównywalny z problemem impasu.


Przepraszam, xagyg wyjaśnia to szczegółowo. Nazwą problemu jest „Lost
Wakeup

@Abhay Bansal: Myślę, że brakuje ci faktu, że condition.wait () zwalnia blokadę i odzyskuje ją wątek, który się budzi.
NickV

4

notify()obudzi jeden wątek, a notifyAll()obudzi wszystko. O ile mi wiadomo, nie ma środkowej płaszczyzny. Ale jeśli nie jesteś pewien, co notify()zrobi z twoimi wątkami, użyj notifyAll(). Działa jak urok za każdym razem.


4

Wszystkie powyższe odpowiedzi są poprawne, o ile mogę powiedzieć, więc powiem ci coś jeszcze. W przypadku kodu produkcyjnego naprawdę powinieneś użyć klas w java.util.concurrent. Jest niewiele rzeczy, których nie mogą dla Ciebie zrobić, jeśli chodzi o współbieżność w Javie.


4

notify()pozwala pisać bardziej wydajny kod niż notifyAll().

Rozważ następujący fragment kodu, który jest wykonywany z wielu równoległych wątków:

synchronized(this) {
    while(busy) // a loop is necessary here
        wait();
    busy = true;
}
...
synchronized(this) {
    busy = false;
    notifyAll();
}

Można to uczynić bardziej wydajnym, używając notify():

synchronized(this) {
    if(busy)   // replaced the loop with a condition which is evaluated only once
        wait();
    busy = true;
}
...
synchronized(this) {
    busy = false;
    notify();
}

W przypadku dużej liczby wątków lub jeśli ocena pętli oczekiwania jest kosztowna, notify()będzie ona znacznie szybsza niż notifyAll(). Na przykład, jeśli masz 1000 wątków, 999 wątków zostanie przebudzonych i ocenionych po pierwszym notifyAll(), a następnie 998, a następnie 997 i tak dalej. Przeciwnie, w przypadku notify()rozwiązania obudzi się tylko jeden wątek.

Użyj, notifyAll()gdy musisz wybrać, który wątek wykona następnie pracę:

synchronized(this) {
    while(idx != last+1)  // wait until it's my turn
        wait();
}
...
synchronized(this) {
    last = idx;
    notifyAll();
}

Na koniec ważne jest, aby zrozumieć, że w przypadku przebudzonego notifyAll()kodu w synchronizedblokach będzie on wykonywany sekwencyjnie, a nie jednocześnie. Powiedzmy, że w powyższym przykładzie czekają trzy wątki i wywołania czwartego wątku notifyAll(). Wszystkie trzy wątki zostaną przebudzone, ale tylko jeden rozpocznie wykonywanie i sprawdzi stan whilepętli. Jeśli warunek jest spełniony true, zadzwoni wait()ponownie, i dopiero wtedy drugi wątek zacznie działać i sprawdzi whilestan pętli itd.


4

Oto prostsze wyjaśnienie:

Masz rację, niezależnie od tego, czy użyjesz replace (), czy notyfikujAll (), natychmiastowy wynik jest taki, że dokładnie jeden inny wątek przejmie monitor i rozpocznie wykonywanie. (Zakładając, że niektóre wątki zostały w rzeczywistości zablokowane podczas oczekiwania () dla tego obiektu, inne niezwiązane wątki nie pochłaniają wszystkich dostępnych rdzeni itp.) Wpływ pojawia się później.

Załóżmy, że wątki A, B i C czekają na ten obiekt, a wątek A pobiera monitor. Różnica polega na tym, co dzieje się, gdy A zwolni monitor. Jeśli użyłeś powiadomienia (), wówczas B i C nadal są blokowane w funkcji wait (): nie czekają na monitorze, czekają na powiadomienie. Kiedy A zwolni monitor, B i C nadal będą tam siedzieć i czekać na powiadomienie ().

Jeśli użyto funkcji replaceAll (), zarówno B, jak i C przeszły obok stanu „czekaj na powiadomienie” i oba czekają na uzyskanie monitora. Kiedy A zwolni monitor, B lub C go przejmie (zakładając, że żaden inny wątek nie konkuruje o ten monitor) i rozpocznie wykonywanie.


Bardzo jasne wytłumaczenie. Rezultat tego zachowania funkcji notuj () może prowadzić do „Nieodebranego sygnału” / „Nieudanego powiadomienia”, co prowadzi do impasu / braku postępu w stanie aplikacji. Producent, C-Konsument P1, P2 i C2 czekają na C1. Połączenia C1 zawiadamiają () i są przeznaczone dla producenta, ale C2 może zostać obudzone, więc zarówno P1, jak i P2 przeoczyły powiadomienie i będą czekały na dalsze wyraźne „powiadomienie” (wywołanie powiadomienia ()).
user104309,

4

Ta odpowiedź jest graficznym przepisaniem i uproszczeniem doskonałej odpowiedzi xagyg , w tym komentarzy Erana .

Dlaczego warto korzystać z funkcji powiadomień, nawet jeśli każdy produkt jest przeznaczony dla jednego konsumenta?

Rozważ producentów i konsumentów, uproszczonych w następujący sposób.

Producent:

while (!empty) {
   wait() // on full
}
put()
notify()

Konsument:

while (empty) {
   wait() // on empty
}
take()
notify()

Załóżmy, że 2 producentów i 2 konsumentów dzieli bufor o rozmiarze 1. Poniższy obraz przedstawia scenariusz prowadzący do impasu , którego można by uniknąć, gdyby wszystkie użyte wątki powiadomiły AllAll .

Każde powiadomienie jest oznaczone budzącym się wątkiem.

impas z powodu powiadomienia


3

Chciałbym wspomnieć o tym, co zostało wyjaśnione w praktyce Java Concurrency w praktyce:

Pierwszy punkt, czy powiadomić czy powiadomić wszystkie?

It will be NotifyAll, and reason is that it will save from signall hijacking.

Jeśli dwa wątki A i B czekają w różnych stanach predykatów tej samej kolejki warunków i wywoływane jest powiadomienie, to do maszyny JVM zostanie powiadomiony wątek JVM.

Teraz, jeśli powiadomienie było przeznaczone dla wątku A, a JVM powiadomił wątek B, to wątek B obudzi się i zobaczy, że to powiadomienie nie jest przydatne, więc zaczeka ponownie. Wątek A nigdy nie dowie się o tym pominiętym sygnale i ktoś porwał jego powiadomienie.

Wywołanie funkcji powiadomień Wszystkie rozwiąże ten problem, ale znowu będzie miało wpływ na wydajność, ponieważ powiadomi wszystkie wątki, a wszystkie wątki będą rywalizować o tę samą blokadę, co będzie wymagało zmiany kontekstu, a tym samym obciążenia procesora. Ale powinniśmy dbać o wydajność tylko wtedy, gdy zachowuje się poprawnie, jeśli samo zachowanie nie jest poprawne, to wydajność nie ma sensu.

Ten problem można rozwiązać za pomocą obiektu warunku jawnego blokowania blokady, dostarczonego w jdk 5, ponieważ zapewnia on inne oczekiwanie na predykat każdego warunku. Tutaj będzie się zachowywał poprawnie i nie będzie problemu z wydajnością, ponieważ wywoła sygnał i upewni się, że tylko jeden wątek czeka na ten warunek


3

notify()- Wybiera losowy wątek z zestawu oczekiwania obiektu i ustawia go w BLOCKEDstanie. Pozostałe wątki w zestawie oczekiwania obiektu są nadal w WAITINGstanie.

notifyAll()- Przenosi wszystkie wątki z zestawu oczekiwania obiektu do BLOCKEDstanu. Po użyciu notifyAll()nie ma żadnych wątków w zestawie oczekiwania obiektu udostępnionego, ponieważ wszystkie są teraz w BLOCKEDstanie, a nie w WAITINGstanie.

BLOCKED- zablokowany w celu uzyskania blokady. WAITING- oczekiwanie na powiadomienie (lub zablokowane na zakończenie dołączenia).


3

Zaczerpnięte z bloga na Effective Java:

The notifyAll method should generally be used in preference to notify. 

If notify is used, great care must be taken to ensure liveness.

Tak więc rozumiem (z wyżej wspomnianego bloga, komentarz „Yann TM” na temat przyjętej odpowiedzi i dokumentów Java ):

  • powiadom (): JVM budzi jeden z oczekujących wątków na tym obiekcie. Wybór wątku jest dokonywany arbitralnie bez uczciwości. Tak więc tę samą nić można budzić raz po raz. Zatem stan systemu zmienia się, ale nie dokonuje się żadnego rzeczywistego postępu. W ten sposób tworzy się livelock .
  • replaceAll (): JVM budzi wszystkie wątki, a następnie wszystkie wątki ścigają się dla blokady tego obiektu. Teraz program planujący procesor wybiera wątek, który uzyskuje blokadę tego obiektu. Ten proces selekcji byłby znacznie lepszy niż wybór JVM. W ten sposób zapewniając żywotność.

2

Spójrz na kod opublikowany przez @xagyg.

Załóżmy, że dwa różne gwinty czeka na dwóch różnych warunkach: pierwszy gwint czeka , a drugi gwint czeka
buf.size() != MAX_SIZEbuf.size() != 0 .

Załóżmy, że w pewnym momencie buf.size() nie jest równy 0 . JVM wywołuje notify()zamiast notifyAll(), a pierwszy wątek jest powiadamiany (nie drugi).

Pierwszy wątek jest budzony, sprawdza, buf.size()które mogą wrócić MAX_SIZEi wraca do oczekiwania. Drugi wątek nie jest budzony, nadal czeka i nie dzwoni get().


1

notify()budzi pierwszy wątek, który wywołał wait()ten sam obiekt.

notifyAll()budzi wszystkie wątki, które wywołały wait()ten sam obiekt.

Wątek o najwyższym priorytecie zostanie uruchomiony jako pierwszy.


13
W przypadku, notify()gdy nie jest to dokładnie „ pierwszy wątek ”.
Bhesh Gurung,

6
nie możesz przewidzieć, który zostanie wybrany przez VM. Tylko Bóg wie.
Sergii Szewczyk,

Nie ma gwarancji, kto będzie pierwszy (bez uczciwości)
Ivan Voroshilin

Pierwszy wątek obudzi się tylko wtedy, gdy system operacyjny to zagwarantuje, i prawdopodobnie tak nie jest. Naprawdę odkłada na system operacyjny (i jego harmonogram), aby ustalić, który wątek się obudzić.
Paul Stelian

1

powiadomienie powiadomi tylko jeden wątek, który jest w stanie oczekiwania, a powiadomienie wszystkich spowoduje powiadomienie wszystkich wątków w stanie oczekiwania, teraz wszystkie zgłoszone wątki i wszystkie zablokowane wątki kwalifikują się do blokady, z których tylko jeden otrzyma zamek i wszystkie pozostałe (w tym te, które są w stanie oczekiwania wcześniej) będą w stanie zablokowanym.


1

Podsumowując powyższe doskonałe szczegółowe wyjaśnienia oraz w najprostszy sposób, jaki mogę wymyślić, wynika to z ograniczeń wbudowanego monitora JVM, który 1) jest nabywany w całej jednostce synchronizacji (bloku lub obiekcie) i 2) nie dyskryminuje konkretnego warunku, na który czeka się / powiadamia się o / o.

Oznacza to, że jeśli wiele wątków czeka na różne warunki i zostanie użyta opcja notyfikacji (), wybrany wątek może nie być tym, który zrobiłby postęp w nowo spełnionym warunku - powodując ten wątek (i inne aktualnie oczekujące wątki, które byłyby w stanie aby spełnić warunek itp.), aby nie być w stanie robić postępów, i ostatecznie głodować lub zawiesić się program.

W przeciwieństwie do tego, enableAll () umożliwia wszystkim oczekującym wątkom ponowne przejęcie blokady i sprawdzenie ich odpowiedniego stanu, umożliwiając w końcu dokonanie postępu.

Tak więc powiadomienie () może być bezpiecznie używane tylko wtedy, gdy gwarantowany jest każdy oczekujący wątek, który umożliwi dokonanie postępu, jeśli zostanie on wybrany, co zasadniczo jest spełnione, gdy wszystkie wątki w tym samym monitorze sprawdzają tylko jeden i ten sam warunek - dość rzadki w rzeczywistych aplikacjach.


0

Gdy wywołasz funkcję wait () „obiektu” (oczekując, że uzyskano blokadę obiektu), stażysta zwolni blokadę tego obiektu i pomoże innym wątkom zablokować ten „obiekt”, w tym scenariuszu wystąpi więcej niż 1 wątek czeka na „zasób / obiekt” (biorąc pod uwagę, że inne wątki wydały również oczekiwanie na ten sam powyższy obiekt i po drodze pojawi się wątek, który wypełni zasób / obiekt i wywoła powiadomienie / powiadomienie wszystkich).

Tutaj, gdy wydasz powiadomienie o tym samym obiekcie (z tej samej / drugiej strony procesu / kodu), zwolni to zablokowany i oczekujący pojedynczy wątek (nie wszystkie oczekujące wątki - ten zwolniony wątek zostanie wybrany przez wątek JVM Harmonogram i cały proces uzyskiwania blokady na obiekcie jest taki sam jak zwykły).

Jeśli masz tylko jeden wątek, który będzie współużytkować / pracować nad tym obiektem, możesz użyć samej metody replace () w implementacji oczekiwania-powiadomienia.

jeśli jesteś w sytuacji, gdy więcej niż jeden wątek czyta i pisze na zasobach / obiekcie w oparciu o logikę biznesową, powinieneś przejść do powiadomieniaAll ()

teraz patrzę, jak dokładnie jvm identyfikuje i przerywa oczekujący wątek, gdy wydajemy powiadomienie () na obiekcie ...


0

Chociaż istnieją pewne solidne odpowiedzi powyżej, jestem zaskoczony liczbą nieporozumień i nieporozumień, które przeczytałem. Prawdopodobnie świadczy to o tym, że należy używać java.util.concurrent tak często, jak to możliwe, zamiast próbować pisać własny zepsuty współbieżny kod. Wracając do pytania: podsumowując, najlepszą praktyką jest dzisiaj UNIKANIE powiadomienia () we WSZYSTKICH sytuacjach z powodu problemu z utratą budzenia. Każdy, kto tego nie rozumie, nie powinien mieć prawa pisać kodu współbieżności o znaczeniu krytycznym. Jeśli martwisz się problemami z pasterstwem, bezpiecznym sposobem na przebudzenie jednego wątku naraz jest: 1. Zbudowanie wyraźnej kolejki oczekujących na wątki oczekujące; 2. Niech każdy wątek w kolejce zaczeka na swojego poprzednika; 3. Niech każde wywołanie wątku powiadomiAll () po zakończeniu. Lub możesz użyć Java.util.concurrent. *,


z mojego doświadczenia wynika, że ​​użycie funkcji oczekiwania / powiadomienia jest często stosowane w mechanizmach kolejek, w których wątek ( Runnableimplementacja) przetwarza zawartość kolejki. Jest wait()on następnie używany, gdy kolejka jest pusta. I notify()jest wywoływany, gdy informacje są dodawane. -> w takim przypadku jest tylko jeden wątek, który kiedykolwiek wywołuje ten wątek wait(), więc czy nie wygląda to trochę głupio, notifyAll()jeśli wiesz, że jest tylko jeden wątek.
bvdb

-2

Przebudzenie wszystkiego nie ma tu większego znaczenia. poczekaj powiadom i powiadom wszystkie, wszystkie są umieszczane po posiadaniu monitora obiektu. Jeśli wątek jest na etapie oczekiwania i wywołane zostanie powiadomienie, wątek przejmie blokadę i żaden inny wątek w tym momencie nie będzie mógł go zablokować. Dlatego równoczesny dostęp nie może mieć miejsca. O ile mi wiadomo, każde połączenie oczekujące powiadomienie i powiadomienie można wykonać dopiero po przejęciu blokady obiektu. Popraw mnie, jeśli się mylę.

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.