11.7.4 | Synchronisation mit Ereignissen |
Zusätzlich zur Synchronisation mit dem Schlüsselwort synchronized bietet Java die Möglichkeit, Threads mit Ereignissen zu synchronisieren.
Hierzu definiert die Klasse Object die Methoden wait() und notify(). Sie geben dem Programmierer eine noch größere Kontrolle über die Abläufe im Zusammenhang mit einem Monitor. Falls beispielsweise der Zugang zu einem Monitor von bestimmten Zuständen bzw. Werten von Datenelementen abhängen soll, so muss es eine Möglichkeit geben, diese Werte innerhalb einer Zugangsmethode eines Monitors zu prüfen und gegebenenfalls den Monitor wieder freizugeben und auf das Eintreffen des richtigen Zustands zu warten.
In Lehrbüchern zeigt man dies oft an dem Beispiel des Produzenten-Konsumenten-Problems. Darunter stellt man sich zwei parallele Threads vor, wobei der eine Daten produziert, die er in einen Puffer hineinstellt, und der andere diese Daten aus dem Puffer entnimmt und weiterverarbeitet. Wenn man sich vorstellt, dass der Puffer eine komplexere Struktur, beispielsweise eine Warteschlange mit einem Zähler ist, so ist offensichtlich, dass das Hinzufügen oder Entfernen eines Elements aus dem Puffer aus mehreren Schritten besteht, die sequenziell ablaufen müssen und während deren Ausführung keine andere Änderung der Warteschlange erfolgen kann. Man kann sich also das Hinzufügen und Entfernen als Zugangsfunktionen zu einem Monitor mit dem Puffer als geschützte Daten vorstellen.
In einer Realisierung würde ein Konsumenten-Thread regelmäßig die Funktion Entfernen aufrufen und ein Produzenten-Thread die Funktion Hinzufügen. Da der Konsumenten-Thread nur dann sinnvoll arbeiten kann, wenn auch Daten im Puffer vorhanden sind, reicht es nicht aus, das gleichzeitige Ablaufen von Entfernen und Hinzufügen über synchronized zu verhindern. Der Konsumenten-Thread würde sonst regelmäßig versuchen, aus dem leeren Puffer Daten zu entnehmen. Man benötigt also eine Möglichkeit, im Falle eines leeren Puffers den Konsumenten-Thread auf das Eintreffen neuer Daten warten zu lassen. Dies erreicht man mit der Methode wait().
Sie stellt den aktiven Thread in eine interne Warteschlange des Monitors, bis ein anderer Thread die Methode notify() des Monitors aufruft. Eine zweite Variante von wait() erlaubt die Angabe einer Zeitspanne, die maximal gewartet werden soll. Falls das Ereignis innerhalb dieser Zeit nicht eintritt, setzt der Thread seine Ausführung fort.
wait() und notify() können nur in Methoden mit dem Schlüsselwort synchronized aufgerufen werden. Dies klingt zunächst etwas verwirrend, da ja immer nur ein Thread gleichzeitig in einer solchen Methode aktiv sein kann. Wenn also ein Thread den Monitor besitzen und mit wait() auf einen anderen Thread warten würde, so hätte kein anderer Thread die Chance, eine Zugangsmethode aufzurufen und den anderen mit notify() zu aktivieren. Doch wait() hat die Eigenschaft, den Monitor automatisch wieder freizugeben, so dass dieses Problem nicht auftritt.
Zur Demonstration des Zusammenspiels zwischen wait() und notify() soll eine Ablage dienen, die eine Zahl aufnehmen kann. Die Ablage verfügt über die Methoden put() und get(), mit denen etwas abgelegt bzw. abgeholt werden kann. Diese beiden Methoden werden jeweils zehnmal von zwei getrennten Threads aufgerufen.
Die Methoden put() und get() werden als synchronized deklariert, damit sie nicht gleichzeitig ausgeführt werden.
Um zu verhindern, dass ein neuer Eintrag erfolgt, bevor der vorherige abgeholt ist, ruft put() die Methode wait() auf, falls die Ablage belegt ist. Der aufrufende Thread muss so lange warten, bis ein notify() kommt. In get() wird eine analoge Absicherung gegen das Lesen getroffen, falls die Ablage leer ist.
Wenn die Ablage leer ist, wird in put() ein Eintrag in die Ablage gelegt und anschließend notify() aufgerufen, um den in get() wartenden Threads zu signalisieren, dass ein neuer Eintrag vorliegt, der abgeholt werden kann.
Symmetrisch führt auch get() am Ende notify() aus, falls Threads in put() darauf warten, dass die Ablage wieder frei wird.public synchronized void put(int data) { if (!empty) try { wait(); } catch(InterruptedException e) {} System.out.println("Eingetragen: "+data); buffer = data; empty = false; notify(); } public synchronized int get() { if (empty) try { wait(); } catch(InterruptedException e) {} System.out.println("Abgeholt: "+buffer); empty = true; notify(); return buffer; }Allgemein bewirkt notify(), dass nur einer der wartenden Threads die Ausführung hinter dem wait()-Aufruf fortsetzen kann. Welcher das ist, darüber macht die Sprachspezifkation keine Vorgaben. Falls es also mehrere Produzenten-Threads gäbe, würde trotzdem nur höchstens einer in der Methode put() sein.
Für Anwendungsfälle, in denen nicht nur einer, sondern alle wartenden Threads vom Eintritt des Ereignisses benachrichtigt werden sollen, definiert Object die Methode notifyAll().
Als nächstes werden die Threads Producer und Consumer implementiert. Ihnen wird im Konstruktor die Ablage übergeben, mit der sie operieren sollen. Die Methode run() wird so überschrieben, dass zehnmal hintereinander put() bzw. get() aufgerufen wird.
Das Testprogramm gestaltet sich sehr einfach. Zunächst werden Objekte der Klassen Depot, Producer und Consumer erzeugt. Anschließend werden die beiden Threads gestartet.
Die Ausgabe dieses Programms sieht folgendermaßen aus:Eingetragen: 0 Abgeholt: 0 Eingetragen: 1 Abgeholt: 1 Eingetragen: 2 Abgeholt: 2 Eingetragen: 3 Abgeholt: 3 Eingetragen: 4 Abgeholt: 4 Eingetragen: 5 Abgeholt: 5 Eingetragen: 6 Abgeholt: 6 ...Die Deklaration von put() und get() als synchronized hat also in Kombination mit der Verwendung von wait() und notify() einen ordnungsgemäßen Ablauf gewährleistet. Wenn man alle diese Zusätze entfernt, sieht das Ergebnis so oder ähnlich aus:Abgeholt: 0 Abgeholt: 0 Eingetragen: 3 Abgeholt: 3 Eingetragen: 5 Abgeholt: 5 Eingetragen: 9An diesem Auszug sieht man unter anderem, dass am Anfang der Konsumenten-Thread ausgeführt wurde, ohne dass der Produzent vorher an der Reihe war.Material zum Beispiel
- Quelltexte:
Um dasDebuggen von parallelen Anwendungen zu vereinfachen und zur Verwendung in Zusicherungen wurde Thread in Version 1.4 um die Methode holdsLock(Object) erweitert. Mit dieser Methode kann abgefragt werden, ob der momentan ausgeführte Thread die Sperre für das übergebene Objekt besitzt. Sie kann unabhängig vom verwendeten Synchronisationsmechanismus (Klassen- oder Objektbezogene Sperre, Ereignisse oder Terminierung) verwendet werden.
In den nachfolgenden Fragmenten wurde das vorhergehende Beispiel um Statusabfragen für die Sperre der Ablage erweitert. Die Abfrage wurde zum einen innerhalb der kritischen Abschnitte der Zugangsmethoden (get() und put()) sowie jeweils vor den Aufrufen der Zugangsmethoden platziert. Wenn die Implementierung korrekt ist, dürfen beide Threads (Produzent und Konsument) die Sperre nur innerhalb der Zugangsmethoden besitzen.
Die um die Statusabfrage erweitere Methode get() hat nun folgende Gestalt (analog bei put()):public synchronized int get() { if (empty) try { wait(); } catch(InterruptedException e) {} // Überprüfung der Sperre System.out.println("Get: Sperrstatus des Threads " +Thread.currentThread().getName()+": " +Thread.holdsLock(this)); System.out.println("Abgeholt: "+buffer); empty = true; notify(); return buffer; }
Damit im Monitor unterschieden werden kann, welcher der beiden Threads gerade ausgeführt wird, werden Produzenten- und Konsumenten-Thread in ihren Konstruktoren mit Namen versehen. Vor dem Aufruf der Zugangsmethoden wird noch einmal der Status der Sperre geprüft:public class Consumer extends Thread { Depot depot; public Consumer(Depot depot) { // Thread-Name im Konstruktor setzen super("Consumer"); // Referenz auf die Ablage merken this.depot = depot; } public void run() { for (int i = 0; i < 10; i++) { // Sperrstatus abfragen und ausgeben System.out.println("Sperrstatus Konsument: " +Thread.holdsLock(depot)); depot.get(); try { sleep((int)(Math.random()*1000)); } catch (InterruptedException e) {} } } }Beim Aufruf von holdsLock() muss darauf geachtet werden, dass die Referenz auf das Sperrobjekt immer korrekt übergeben wird. Innerhalb des Monitors wurde im Beispiel this angegeben, im Produzenten- und im Konsumenten-Thread dagegen die gespeicherte Referenz depot. Würde in diesen beiden Threads this übergeben werden, dann würde die Sperre des entsprechenden Thread-Objekts geprüft werden und nicht die der Ablage.
Die Ausgabe eines Zyklus sieht dann wie erwartet aus:Sperrstatus Produzent: false Put: Sperrstatus des Threads Producer: true Eingetragen: 0 Sperrstatus Konsument: false Get: Sperrstatus des Threads Consumer: true Abgeholt: 0