Advanced Java Services | Die Methoden wait() und notify() aus der Klasse Object |
Mit wait() geht ein Thread in einen Wartezustand, der im Normalfall durch ein notify() oder notifyAll() beendet wird. In Ausnahmefällen kann ein wait() auch durch eine Interruptaufforderung beendet werden. wait(), notify() und notifyAll() stehen für jedes Objekt zur Verfügung können aber nur in synchronisierten Codeabschnitten aufgerufen werden, andernfalls werfen sie eine IllegalMonitorStateException. Es ist wichtig, daran zu erinnern, daß wait() den Lock zurückgibt.
Das Beispiel zeigt einen Thread, der zu einem Objekt wait() aufruft. wait() blockiert bis ein anderer Thread zum selben Objekt ein notify aufruft.
Der Thread
import java.time.LocalTime; public class Waiter extends Thread { private Object ob; /** */ public Waiter(Object ob) { this.ob = ob; } public void run() { // waiter warten sofort synchronized(ob) { try { System.out.println(this.getName() + " waits"); ob.wait(); System.out.println(this.getName() + " ends waiting"); } catch(InterruptedException ex) { System.out.println(this.getName() + " " +ex); } } } }
Die Mainklasse
main legt ein Object an und übergibt es an drei Threads. Nach dem Start wartet main eine Sekunde und weckt mit notify einen der wartenden Threads. Die anderen beiden werden zwei Sekunden später durch Interrupt-Aufrufe beendet. Da sie keine Dämonen sind würde ansonsten main nicht enden.
public class Wait_Notify_1 { public static void main(String[] args) { Object ob = new Object(); Waiter wt1 = new Waiter(ob); Waiter wt2 = new Waiter(ob); Waiter wt3 = new Waiter(ob); wt1.start(); wt2.start(); wt3.start(); try { TimeUnit.SECONDS.sleep(1); } catch(InterruptedException ex) {} synchronized(ob) { System.out.println("main calls notify"); ob.notify(); } try { TimeUnit.SECONDS.sleep(2); } catch(InterruptedException ex) {} System.out.println("main interrupts all waiting threads"); wt1.interrupt(); wt2.interrupt(); wt3.interrupt(); } }
Die Ausgabe
Thread-0 waits Thread-1 waits Thread-2 waits main calls notify Thread-0 ends waiting main interrupts all waiting threads Thread-1 java.lang.InterruptedException Thread-2 java.lang.InterruptedException
Man beachte, daß wait() und notify() zum gleichen Objekt synchronisiert sind. Das ist notwendig. Eine Synchronistion zu verschiedenen Objekten führt ebenfalls zu einer IllegalMonitorStateException.
Der Waiter ist identisch zum ersten Beispiel.
In der Mainklasse legen wir nun ein Objekt an und wieder drei Threads, die dieses Objekt erhalten. Diesmal aber wird notifyAll gerufen.
public class Wait_Notify_01 { public static void main(String[] args) { Object ob = new Object(); Waiter w0 = new Waiter(ob); Waiter w1 = new Waiter(ob); Waiter w2 = new Waiter(ob); w0.start(); w1.start(); w2.start(); try { TimeUnit.SECONDS.sleep(2); } catch(InterruptedException ex) {} synchronized(ob) { System.out.println("main calls notifyAll"); ob.notifyAll(); } } }
Hier drei mögliche Ausgaben des Programms
Thread-0 waits Thread-2 waits Thread-1 waits main calls notifyAll Thread-1 ends waiting 15:17:53.271 Thread-2 ends waiting 15:17:53.279 Thread-0 ends waiting 15:17:53.279
Thread-0 waits Thread-1 waits Thread-2 waits main calls notifyAll Thread-2 ends waiting 15:21:03.761 Thread-1 ends waiting 15:21:03.769 Thread-0 ends waiting 15:21:03.769
Da wait() den Lock zurückgibt eignet es sich gut für sogenante Consumer-Producer Modelle. Consumer und Producer sind Threads die ein Lager leeren oder füllen, d.h. sie greifen auf ein gemeinsames Lager zu. Das Lager bietet put() und get() Methoden an mit denen man Waren im Lager deponieren oder entnehmen kann. put() und get() können mit wait() in einen Wartezustand gehen je nachdem ob das Lager voll oder leer ist. Daher sind put() und get() synchronized.
Die klassische Lösung des Consumer-Producer-Modells ist zugleich simpel und raffiniert. Diese Lösung arbeitet mit einer einzigen booleschen Variablen, die meist available heißt. Die put()-Methode "denkt" sich, "solange was da ist, braucht man nichts Auffüllen, also warten", und die get()-Methode denkt sich "solange nichts da ist, kann nichts abgeholt werden, also warten". In den put()- und get()-Methoden wechseln sich also wait() und notify() ab.
In diesem Beispiel leeren (Consumer) oder füllen (Producer) das Lager in unregelmäßigen Abständen quasi endlos. Dies wird durch unterschiedliche sleep()-Zeiten und eine Endlosschleife realisiert.
In main() soll das Programm durch Interrupt-Aufrufe für Consumer und Producer beendet werden. Die Interrupts können die Threads in zwei verschiedenen Zuständen erreichen, in der sleep()-Phase oder in der wait()-Phase. In beiden Fällen soll die Anwendung beendet werden, hierbei fordert die Beendigung durch Unterbrechung der wait()-Phase den größeren Aufwand. Das Beenden während der wait()-Phase wird durch geeignete Returnwerte der get()- und put()-Methode erreicht.
Das Lager sieht daher folgendermaßen aus.
public class Stock { private int MAX = 10; // maximaler Bestand private int content; // aktueller Lagerbestand private boolean available = false; public Stock(int MAX) { this.MAX = MAX; this.content = this.MAX - 1; } public synchronized int put() { putCount++; // solange was da, braucht nicht aufgefüllt werden, also warten while(available == true) { System.out.println(Thread.currentThread().getName() + " must wait"); try { wait(); } catch(InterruptedException ex) { return -1; } } //hier ist available = false System.out.println(Thread.currentThread().getName() + " ends waiting"); content++; // dadurch wird available true ! System.out.println(Thread.currentThread().getName() + " puts 1 content = " + content); available = true; notifyAll(); return 1; } public synchronized int get() { // solange nichts da ist, kann nichts abgeholt werden, also warten while(available == false) { System.out.println(Thread.currentThread().getName() + " must wait"); try { wait(); } catch(InterruptedException ex) { return -1; } } System.out.println(Thread.currentThread().getName() + " ends waiting"); // available = true content--; System.out.println(Thread.currentThread().getName() + " gets 1 content = " + content); available = false; notifyAll(); return 1; } }
Der Consumer Falls der Interrupt während der wait-Phase ankommt, liefert get -1 zurück.
import java.util.concurrent.TimeUnit; public class Consumer extends Thread { private Stock lager; private int sleepTime; public Consumer(Stock c, String name, int sleepTime) { lager = c; this.setName(name); this.sleepTime = sleepTime; } public void run() { int value=0; for(;;) { value = lager.get(); // 1 abholen, blockiert if (value==-1) // get hat interrupt bekommen { System.out.println("consumer: wait interrupted, end consumer"); return; } try { //TimeUnit.MILLISECONDS.sleep(1 + (int)(Math.random()*sleepTime)); Thread.sleep(sleepTime); } catch(InterruptedException ex) { interrupt(); System.out.println("consumer: sleep interrupted, end consumer"); break; } } } }
Der Producer Falls der Interrupt während der wait-Phase ankommt, liefert get -1 zurück.
import java.util.concurrent.TimeUnit; public class Producer extends Thread { private Stock lager; private int sleepTime; public Producer(Stock lager, String name, int sleepTime) { this.lager = lager; this.setName(name); this.sleepTime = sleepTime; } public void run() { int ret = 0; for(;;) { ret = lager.put(); // auffüllen um 1, blockiert if (ret == -1) // put hat interrupt bekommen { System.out.println("producer: wait interrupted, end producer"); return; } try { //TimeUnit.MILLISECONDS.sleep(1 + (int)(Math.random()*sleepTime)); Thread.sleep(sleepTime); } catch(InterruptedException ex) { interrupt(); System.out.println("producer: sleep interrupted, end producer"); break; } } } }
Main
public class Main { public static void main(String[] args) { Stock lager = new Stock(5); Consumer c1 = new Consumer(lager, "consumer", 20); // Consumer will mehr verbrauchen als produziert wird Producer p1 = new Producer(lager, "producer", 100); p1.start(); c1.start(); try { Thread.sleep(500); c1.interrupt(); p1.interrupt(); } catch(InterruptedException e) { e.printStackTrace(); } } }
producer ends waiting producer puts 1 content = 5 consumer ends waiting consumer gets 1 content = 4 consumer must wait producer ends waiting producer puts 1 content = 5 consumer ends waiting consumer gets 1 content = 4 consumer must wait producer ends waiting producer puts 1 content = 5 consumer ends waiting consumer gets 1 content = 4 consumer must wait producer ends waiting producer puts 1 content = 5 consumer ends waiting consumer gets 1 content = 4 consumer must wait consumer: wait interrupted, end consumer producer: sleep interrupted, end producer
Hier kann es vorkommen, daß die Interrupts Consumer und Producer in der sleep-Phase unterbrechen
producer ends waiting producer puts 1 content = 5 consumer ends waiting consumer gets 1 content = 4 consumer must wait producer ends waiting producer puts 1 content = 5 consumer ends waiting consumer gets 1 content = 4 consumer must wait producer ends waiting producer puts 1 content = 5 consumer ends waiting consumer gets 1 content = 4 consumer must wait producer ends waiting producer puts 1 content = 5 consumer ends waiting consumer gets 1 content = 4 consumer must wait producer ends waiting producer puts 1 content = 5 consumer ends waiting consumer gets 1 content = 4 consumer must wait producer ends waiting producer puts 1 content = 5 consumer ends waiting consumer gets 1 content = 4 consumer must wait producer ends waiting producer puts 1 content = 5 consumer ends waiting consumer gets 1 content = 4 producer: sleep interrupted, end producer consumer: sleep interrupted, end consumer