poniedziałek, października 05, 2009

wait() i notify()/notifyAll() - najbardziej nierozumiane metody klasy Object ;-)

Zrobiłem sobie wersję 1.8.1 OOo2GD, ale "jak zwykle" strona rozszerzeń padła więc nie mogę jej tam wrzucić [ale dostępna jest na stronie rozszerzenia ;-)], a że męczy mnie katar to chciałbym sobie coś zakodować albo chociaż napisać coś na blogu [w tracie pisania strona zaczęła znów działać ;-)].

Będzie więc dziś o chyba 2 najbardziej nierozumianych metodach klasy Object. Czyli o wait() i notify()/notifyAll().
Spróbuję to wytłumaczyć prostymi słowami ;-)

Po pierwsze wolno tych metod używać tylko w sekcji synchronized, tudzież w metodach synchronizowanych i wołać je wolno tylko z obiektów na których odbywa się synchronizacja.
public class Fred { 
public static void main(String[] args) {
final Fred fred = new Fred();
final Thread t1 = new Thread(new Runnable() {
public void run() {
System.out.println("t1 start :-)");
System.out.println("t1 will wait...");
synchronized (fred) {
try {
fred.wait();
} catch (InterruptedException ie) {
System.out.println("t1 interrupted...");
}
}
System.out.println("t1 stop :-)");
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
public void run() {
System.out.println("t2 start :-)");
synchronized (fred) {
System.out.println("t2 notifyAll!!!");
fred.notifyAll();
System.out.println("t2 done ;-)");
}
System.out.println("t2 stop :-)");
}
});
t2.start();
}
}


Co się dzieje w tym programie wyżej?
Najpierw tworzymy obiekt, na którym będziemy się synchronizować, zwany jest on dla niepoznaki monitorem... OK, dokładniej monitor jest na obiekcie, ale to szczegół.
Następnie tworzymy sobie wątek t1, który ma za zadanie wypisać parę rzeczy i następnie ma wywołać na obiekcie fred metodę wait(). Po tym wszystkim startujemy wątek t1 [tak naprawdę mówimy tylko, że chcielibyśmy aby ten wątek został uruchomiony], po czym tworzymy wątek t2, który ma zawołać na obiekcie fred metodę notifyAll(). Ma więc obudzić wszystkie wątki które czekają na sygnał na danym monitorze. Gdybyśmy w tym miejscu zawołali notify() to JVM wybudziłaby "któryś" z wątków wiszących na tym monitorze.
W większości systemów wynik działania programu będzie wyglądał tak:
t1 start :-)
t1 will wait...
t2 start :-)
t2 notifyAll!!!
t2 done ;-)
t1 stop :-)
t2 stop :-)}

Choć mogą być i takie gdzie t2 wystartuje przed t1, znotyfikuje wszystkie wątki wiszące na monitorze [czyli 0], a dopiero wtedy t1 zacznie czekać ;-) Choć jest to jednak mało prawdopodobne. Wątki nie są deterministyczne, ale z dużym przybliżeniem można często bazować na zdrowym rozsądku.

Zamiast używać freda jako jawnego monitora moglibyśmy po prostu w klasie Fred stworzyć 2 metody:
synchronized void method1() {
try {
wait();
} catch (InterruptedException ie) {
}
}

synchronized method2() {
notifyAll();
}

i wołać je z naszych wątków.

Teraz tłumaczenie do czego to się może przydać ;-)

Mamy np. coś co chcemy by wykonywało się co najmniej raz na 5 minut, ale w razie potrzeby żebyśmy mogli to wywołać na żądanie. Może chodzić np. o wysyłanie maili, możemy np. zdecydować, że normalne maile są kolejkowane i raz na 5 minut w orgii mailowej wysyłamy je wszystkie, a maile o błędach czy np. z przypomnieniem haseł wysyłane są na bieżąco.
Wtedy w naszym mailerze możemy użyć wait() i notify() [a dokładniej wait(int)]...
choć i tu mogą pojawić się problemy, możemy np. nieszczęśliwie znotyfikować 0 wątków bo akurat wątek pocztowy wysyła maile....

A co gdy chcemy by wątek poczekał na inny? ;-)
Wtedy używamy metody join() i tym razem bez synchronizacji i wołamy ją na wątku na który chcemy poczekać.
Jeżeli chcemy czekać pewien czas, a później nawet jeśli wątek na który czekamy się jeszcze nie skończył chcemy działać dalej to używamy join(int).

Gdy chcemy by wątek poszedł spać na zadany czas i nie interesuje nas to by go budzić, wtedy używamy sleep(int).

Używajac wait(), wait(int), join(), join(int) i sleep(int) musimy łapać InterruptedException.
Kiedy ten wyjątek może zostać rzucony? Wtedy gdy wątek który woła daną metodę zostanie przerwany. Przyznam, że jedyną mi znaną tego przyczyną może być zawołanie na tym wątku metody interrupt(), ale zapewne JVM jest w stanie czasem spowodować ten wyjątek w innych okolicznościach ;-)
Dlatego gdy chcecie by wątek spał/czekał/oczekiwał przez zadany czas to bezpieczniej wołać te metody w pętli i sprawdzać czy zadany czas już minął.

Nie do końca jestem pewien czy to dobrze wytłumaczyłem.... ;-) Jakby co to pytać w komentarzach :-)


Podobne postybeta
Dodajmy 3 klasy do SDK Java'y ;-)
Modale dobre - confirm dla Androida :-)
Potfór ;-) czyli generator z yield w Java'ie
Przepływ sterowany danymi - A takie Java'owe coś ;-)
Czemu wait() i notify()/notifyAll() w ogóle działają?