Prosty scenariusz z użyciem funkcji wait () i powiadomienia () w java

181

Czy mogę uzyskać kompletny prosty scenariusz, tj. Samouczek sugerujący, w jaki sposób należy go używać, szczególnie w przypadku kolejki?

Olaseni
źródło

Odpowiedzi:

269

wait()Oraz notify()sposoby mają na celu zapewnić mechanizm pozwalający gwint do bloku, aż stan specyficzne spełnione. W tym celu zakładam, że chcesz napisać implementację kolejki blokującej, w której masz zapasowy zapas elementów o stałym rozmiarze.

Pierwszą rzeczą, którą musisz zrobić, to określić warunki, na które metody mają czekać. W takim przypadku będziesz chciał, aby put()metoda blokowała, dopóki nie będzie wolnego miejsca w sklepie, i będziesz chciał, aby take()metoda blokowała, dopóki jakiś element nie zwróci.

public class BlockingQueue<T> {

    private Queue<T> queue = new LinkedList<T>();
    private int capacity;

    public BlockingQueue(int capacity) {
        this.capacity = capacity;
    }

    public synchronized void put(T element) throws InterruptedException {
        while(queue.size() == capacity) {
            wait();
        }

        queue.add(element);
        notify(); // notifyAll() for multiple producer/consumer threads
    }

    public synchronized T take() throws InterruptedException {
        while(queue.isEmpty()) {
            wait();
        }

        T item = queue.remove();
        notify(); // notifyAll() for multiple producer/consumer threads
        return item;
    }
}

Należy zwrócić uwagę na kilka sposobów korzystania z mechanizmów oczekiwania i powiadamiania.

Po pierwsze, należy upewnić się, że wszelkie wywołania wait()lub notify()zsynchronizowane regiony kodu ( wait()lub notify()wywołania i są synchronizowane na tym samym obiekcie). Przyczyną tego (poza standardowymi obawami dotyczącymi bezpieczeństwa wątków) jest coś, co nazywane jest brakującym sygnałem.

Przykładem tego jest to, że wątek może wywoływać, put()gdy kolejka się zapełni, a następnie sprawdza warunek, widzi, że kolejka jest pełna, jednak zanim będzie mogła zablokować inny wątek, zaplanowano. Ten drugi wątek jest wówczas take()elementem z kolejki i powiadamia oczekujące wątki, że kolejka nie jest już pełna. Ponieważ pierwszy wątek już sprawdził ten warunek, po prostu zadzwoni wait()po ponownym zaplanowaniu, nawet jeśli może zrobić postęp.

Synchronizując obiekt współdzielony, możesz upewnić się, że ten problem nie wystąpi, ponieważ take()wywołanie drugiego wątku nie będzie w stanie robić postępu, dopóki pierwszy wątek nie zostanie faktycznie zablokowany.

Po drugie, musisz umieścić warunek sprawdzany w pętli while, a nie instrukcję if, ze względu na problem zwany fałszywymi pobudzeniami. W tym miejscu wątek oczekujący może być czasami ponownie aktywowany bez notify()wywołania. Umieszczenie tej kontroli w pętli while zapewni, że jeśli wystąpi fałszywe budzenie, warunek zostanie ponownie sprawdzony, a wątek zadzwoni wait()ponownie.


Jak wspomniano w niektórych innych odpowiedziach, Java 1.5 wprowadziła nową bibliotekę współbieżności (w java.util.concurrentpakiecie), która została zaprojektowana w celu zapewnienia abstrakcyjnego poziomu wyższego niż mechanizm oczekiwania / powiadamiania. Korzystając z tych nowych funkcji, możesz przepisać oryginalny przykład w następujący sposób:

public class BlockingQueue<T> {

    private Queue<T> queue = new LinkedList<T>();
    private int capacity;
    private Lock lock = new ReentrantLock();
    private Condition notFull = lock.newCondition();
    private Condition notEmpty = lock.newCondition();

    public BlockingQueue(int capacity) {
        this.capacity = capacity;
    }

    public void put(T element) throws InterruptedException {
        lock.lock();
        try {
            while(queue.size() == capacity) {
                notFull.await();
            }

            queue.add(element);
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public T take() throws InterruptedException {
        lock.lock();
        try {
            while(queue.isEmpty()) {
                notEmpty.await();
            }

            T item = queue.remove();
            notFull.signal();
            return item;
        } finally {
            lock.unlock();
        }
    }
}

Oczywiście, jeśli rzeczywiście potrzebujesz kolejki blokującej, powinieneś użyć implementacji interfejsu BlockingQueue .

Również w przypadku takich rzeczy bardzo polecam Java Concurrency in Practice , ponieważ obejmuje wszystko, co możesz chcieć wiedzieć o problemach i rozwiązaniach związanych z współbieżnością.

Jared Russell
źródło
7
@greuze, notifybudzi tylko jeden wątek. Jeśli dwa wątki konsumenckie rywalizują o usunięcie elementu, jedno powiadomienie może obudzić drugi wątek konsumencki, co nie może nic z tym zrobić i wróci do trybu uśpienia (zamiast producenta, który, jak mieliśmy nadzieję, wstawi nowy element). Ponieważ wątek producenta nie został przebudzony, nic nie zostało wstawione, a teraz wszystkie trzy wątki będą spały w nieskończoność. Usunąłem mój poprzedni komentarz, ponieważ (niesłusznie) stwierdził, że przyczyną problemu był fałszywy
pobudka
1
@finnw O ile mi wiadomo, problem, który zauważyłeś, można rozwiązać za pomocą powiadomieniaAll (). Czy mam rację?
Clint Eastwood
1
Przykład podany przez @Jared jest całkiem dobry, ale ma poważny upadek. W kodzie wszystkie metody zostały oznaczone jako zsynchronizowane, ale W TYM SAMYM CZASIE NIE MOŻNA WYKONAĆ DWÓCH SYNCHRONIZOWANYCH METOD, więc jak to możliwe, że na zdjęciu jest drugi wątek.
Shivam Aggarwal,
10
@ Brut3Forc3 musisz przeczytać javadoc funkcji wait (): mówi: Wątek zwalnia własność tego monitora . Tak więc, jak tylko zostanie wywołane wait (), monitor zostaje zwolniony, a inny wątek może wykonać inną zsynchronizowaną metodę kolejki.
JB Nizet,
1
@JBNizet. „Przykładem tego jest to, że wątek może wywołać metodę put (), gdy kolejka się zapełni, a następnie sprawdza warunek, widzi, że kolejka jest pełna, jednak zanim będzie mogła zablokować kolejny wątek, zaplanowano”. drugi wątek jest zaplanowany, jeśli jeszcze nie
wywołano oczekiwania
148

Nie przykład kolejki, ale niezwykle prosty :)

class MyHouse {
    private boolean pizzaArrived = false;

    public void eatPizza(){
        synchronized(this){
            while(!pizzaArrived){
                wait();
            }
        }
        System.out.println("yumyum..");
    }

    public void pizzaGuy(){
        synchronized(this){
             this.pizzaArrived = true;
             notifyAll();
        }
    }
}

Kilka ważnych punktów:
1) NIGDY nie rób

 if(!pizzaArrived){
     wait();
 }

Zawsze używaj while (warunek), ponieważ

  • a) wątki mogą sporadycznie wybudzać się ze stanu oczekiwania bez powiadomienia przez nikogo. (nawet jeśli facet od pizzy nie zadzwonił dzwonkiem, ktoś zdecydowałby się spróbować zjeść pizzę).
  • b) Powinieneś sprawdzić stan ponownie po uzyskaniu synchronicznej blokady. Powiedzmy, że pizza nie trwa wiecznie. Obudziłeś się, ustawiłeś się na pizzę, ale to nie wystarczy dla wszystkich. Jeśli nie sprawdzisz, możesz zjeść papier! :) (prawdopodobnie lepszy przykład while(!pizzaExists){ wait(); }.

2) Musisz przytrzymać blokadę (zsynchronizowaną) przed wywołaniem funkcji wait / nofity. Wątki muszą również uzyskać blokadę przed przebudzeniem.

3) Staraj się unikać uzyskiwania blokady w synchronizowanym bloku i staraj się nie wywoływać metod obcych (metod, których nie wiesz na pewno, co robią). Jeśli musisz, koniecznie podejmij środki, aby uniknąć impasu.

4) Uważaj na powiadomienie (). Pozostań przy powiadomieniuAll (), dopóki nie dowiesz się, co robisz.

5) Na koniec przeczytajcie Współbieżność Java w praktyce !

Enno Shioji
źródło
1
Czy mógłbyś wyjaśnić, dlaczego nie używać „if (! PizzaArrived) {wait ();}”?
Wszyscy
2
@ Każdy: Dodano wyjaśnienie. HTH.
Enno Shioji
1
po co używać pizzaArrivedflagi? jeśli flaga zostanie zmieniona bez wywołania, notifynie będzie miała żadnego efektu. Również tylko z waiti notifywywoła przykład działa.
Pablo Fernandez
2
Nie rozumiem - wątek 1 wykonuje metodę eatPizza () i wchodzi do najwyższego zsynchronizowanego bloku i synchronizuje się w klasie MyHouse. Żadna pizza jeszcze nie dotarła, więc po prostu czeka. Teraz wątek 2 próbuje dostarczyć pizzę, wywołując metodę pizzaGuy (); ale nie może, ponieważ wątek 1 jest już właścicielem zamka i nie poddaje się (nieustannie czeka). Skutkiem tego jest zakleszczenie - wątek 1 czeka na wątek 2, aby wykonać metodę replaceAll (), podczas gdy wątek 2 czeka, aż wątek 1 zrezygnuje z blokady klasy MyHouse ... Czego mi brakuje? tutaj?
flamming_python
1
Nie, gdy zmienna jest chroniona przez synchronizedsłowo kluczowe, deklarowanie zmiennej jest zbędne volatilei zaleca się unikanie jej, aby uniknąć pomyłek @mrida
Enno Shioji
37

Nawet jeśli poprosił wait()a notify()konkretnie, czuję, że ten cytat jest nadal ważne, wystarczy:

Josh Bloch, Effective Java 2nd Edition , pozycja 69: Preferuj narzędzia do współbieżności waiti notify(podkreślam jego):

Biorąc pod uwagę trudności związane z użyciem waiti notifypoprawnie, należy użyć narzędzi współbieżności wyższego poziomu zamiast [...] z użyciem waiti notifybezpośrednio jest jak programowanie w „asemblerze współbieżności” w stosunku do języka wyższego poziomu świadczonych przez java.util.concurrent. Rzadko, jeśli w ogóle, jest powód do używania waiti notifynowego kodu .

środki smarujące wielotlenowe
źródło
BlockingQueueS dostarczone w pakiecie java.util.concurrent nie są trwałe. Czego możemy użyć, gdy kolejka musi być trwała? tzn. jeśli system zawiedzie z 20 elementami w kolejce, potrzebuję, aby były one obecne podczas ponownego uruchamiania systemu. Ponieważ wszystkie kolejki java.util.concrent wydają się być „tylko w pamięci”, czy jest jakiś sposób, aby można je było wykorzystać jako / hackowane / zastępowane w celu zapewnienia implementacji zdolnych do trwałości?
Volksman
1
Być może mogłaby być zapewniona kolejka zapasowa? tzn. zapewnimy trwałą implementację interfejsu kolejki.
Volksman
To jest bardzo dobry do wymienionych w tym kontekście, że nigdy nie trzeba używać notify()i wait()ponownie
Chaklader Asfak Arefe
7

Czy obejrzałeś samouczek Java ?

Ponadto radzę trzymać się z daleka od zabawy tego rodzaju rzeczami w prawdziwym oprogramowaniu. Dobrze się z tym bawić, żebyś wiedział, co to jest, ale współbieżność ma swoje pułapki. Lepiej jest stosować abstrakcje wyższego poziomu i zsynchronizowane kolekcje lub kolejki JMS, jeśli budujesz oprogramowanie dla innych osób.

Tak przynajmniej robię. Nie jestem ekspertem w dziedzinie współbieżności, więc w miarę możliwości unikam ręcznej obsługi wątków.

extraneon
źródło
2

Przykład

public class myThread extends Thread{
     @override
     public void run(){
        while(true){
           threadCondWait();// Circle waiting...
           //bla bla bla bla
        }
     }
     public synchronized void threadCondWait(){
        while(myCondition){
           wait();//Comminucate with notify()
        }
     }

}
public class myAnotherThread extends Thread{
     @override
     public void run(){
        //Bla Bla bla
        notify();//Trigger wait() Next Step
     }

}
Ferhat KOÇER
źródło
0

Przykład funkcji wait () i replaceall () w wątku.

Zsynchronizowana statyczna lista tablic jest używana jako zasób i wywoływana jest metoda wait (), jeśli lista tablic jest pusta. Metoda powiadomienie () jest wywoływana po dodaniu elementu do listy tablic.

public class PrinterResource extends Thread{

//resource
public static List<String> arrayList = new ArrayList<String>();

public void addElement(String a){
    //System.out.println("Add element method "+this.getName());
    synchronized (arrayList) {
        arrayList.add(a);
        arrayList.notifyAll();
    }
}

public void removeElement(){
    //System.out.println("Remove element method  "+this.getName());
    synchronized (arrayList) {
        if(arrayList.size() == 0){
            try {
                arrayList.wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }else{
            arrayList.remove(0);
        }
    }
}

public void run(){
    System.out.println("Thread name -- "+this.getName());
    if(!this.getName().equalsIgnoreCase("p4")){
        this.removeElement();
    }
    this.addElement("threads");

}

public static void main(String[] args) {
    PrinterResource p1 = new PrinterResource();
    p1.setName("p1");
    p1.start();

    PrinterResource p2 = new PrinterResource();
    p2.setName("p2");
    p2.start();


    PrinterResource p3 = new PrinterResource();
    p3.setName("p3");
    p3.start();


    PrinterResource p4 = new PrinterResource();
    p4.setName("p4");
    p4.start();     

    try{
        p1.join();
        p2.join();
        p3.join();
        p4.join();
    }catch(InterruptedException e){
        e.printStackTrace();
    }
    System.out.println("Final size of arraylist  "+arrayList.size());
   }
}
srinivas
źródło
1
proszę sprawdzić tę linię if(arrayList.size() == 0), myślę, że może to być pomyłka.
Wizmann