![]() |
|
|
9.9.4 wait() mit einer Zeitspanne
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class java.lang.Object |
| void wait( long timeout ) throws InterruptedException Wartet auf ein notify()/notifyAll() eine gegebene Anzahl von Millisekunden. Nach der abgelaufenen Zeit geht es ohne Fehler weiter. |
| void wait( long timeout, int nanos ) throws InterruptedException Wartet auf ein notify()/notifyAll() angenähert 1.000.000 * timeout + nanos Nano-Sekunden. |
Beispiel Warte maximal zwei Sekunden auf die Daten vom Objekt o1. Wenn diese nicht ankommen, versuche notify()/notifyAll() vom Objekt o2 zu bekommen.
o1.wait( 2000 ); o2.wait();Die Synchronisationsblöcke müssen bei einem lauffähigen Beispiel noch hinzugefügt werden. |
Ein kleines Erzeuger-Verbraucher-Programm soll die Anwendung von Threads kurz demonstrieren. Zwei Threads greifen auf eine gemeinsame Datenbasis zurück. Ein Thread produziert unentwegt Daten (in dem Beispiel ein Zeit-Datum) und schreibt diese in einen Vektor. Der andere Thread nimmt Daten aus dem Vektor heraus und schreibt diese auf den Bildschirm.

Hier klicken, um das Bild zu Vergrößern
Listing 9.14 ErzeugerVerbraucherDemo.java, Teil 1
import java.util.LinkedList; import java.util.Queue; class Erzeuger extends Thread { private final static int MAXQUEUE = 13; private Queue<String> nachrichten = new LinkedList<String>(); public void run() { try { while ( true ) { erzeuge(); sleep( (int)(Math.random()*1000) ); } } catch ( InterruptedException e ) { } } public synchronized void erzeuge() throws InterruptedException { while ( nachrichten.size() == MAXQUEUE ) wait(); nachrichten.add( new java.util.Date().toString() ); notifyAll(); // oder notify(); } // vom Verbraucher aufgerufen public synchronized String verbrauche() throws InterruptedException { while ( nachrichten.size() == 0 ) wait(); notifyAll(); return nachrichten.poll(); } }
Die gesamte Klasse Erzeuger erweitert Thread. Als Objektvariable wird eine Warteschlange als Objekt vom Typ Queue definiert, das die Daten aufnimmt, auf die die Threads dann zurückgreifen. Die erste definierte Funktion ist erzeuge(). Wenn noch Platz in der Warteschlange ist, dann hängt die Funktion das Erstellungsdatum an. Anschließend informiert der Erzeuger über notifyAll() alle eventuell wartenden Threads.
Die Verbraucher nutzen die Funktion verbrauche(). Sind in der Warteschlange keine Daten vorhanden, so wartet der Thread durch wait(). Wichtig ist der Programmblock in der Schleife zum Holen der Nachricht:
while ( nachrichten.size() == MAXQUEUE ) wait();
Es ist typisch für Wartesituationen, dass wait() in einem Schleifenrumpf aufgerufen wird. Denn falls ein notifyAll() aus dem wait() erlöst, kann gleichzeitig auch ein anderer Thread fertig werden und aus der Schleife herauskommen. Das ist der Fall, wenn gleichzeitig Threads auf das Ankommen neuer Güter warten. Ein einfaches if würde dazu führen, dass einer der beiden ein erzeugtes Gut entnimmt und der andere Verbraucher dann keins mehr bekommen kann. Die Schleifenbedingung ist das Gegenteil der Bedingung, auf die gewartet werden soll.
Der Quellcode der Klasse Verbraucher ist kleiner.
class Verbraucher extends Thread { Erzeuger erzeuger; String name; Verbraucher( String name, Erzeuger erzeuger ) { this.erzeuger = erzeuger; this.name = name; } public void run() { try { while ( true ) { String info = erzeuger.verbrauche(); System.out.println( name +" holt Nachricht: "+ info ); Thread.sleep( (int)(Math.random()*1000) ); } } catch ( InterruptedException e ) { } } }
Das Hauptprogramm in ErzeugerVerbraucherDemo konstruiert einen Erzeuger und drei Verbraucher. Wir übergeben im Konstruktor den Erzeuger, und dann holen sich die Verbraucher mittels verbrauche() selbstständig die Daten ab.
public class ErzeugerVerbraucherDemo { public static void main( String args[] ) { Erzeuger erzeuger = new Erzeuger(); erzeuger.start(); new Verbraucher( "Eins", erzeuger ).start(); new Verbraucher( "Zwei", erzeuger ).start(); new Verbraucher( "Drei", erzeuger ).start(); } }
Es ergibt sich eine Ausgabe ähnlich dieser:
Eins holt Nachricht: Tue May 25 14:57:19 CEST 2004 Zwei holt Nachricht: Tue May 25 14:57:20 CEST 2004 Drei holt Nachricht: Tue May 25 14:57:20 CEST 2004 Eins holt Nachricht: Tue May 25 14:57:21 CEST 2004 Zwei holt Nachricht: Tue May 25 14:57:22 CEST 2004 ... bis in die Unendlichkeit
Wir haben gesehen, dass sich Synchronisationsprobleme durch kritische Abschnitte und Wartesituationen mit wait() und notify() lösen lassen. Dennoch ist der eingebaute Mechanismus auch mit Nachteilen verbunden. Denn eine große Schwierigkeit ist es, synchronisierende Programme zu entwickeln und zu warten. Die Synchronisationsvariablen verstreuen sich mitunter über große Programmteile und machen die Wartung schwierig. Teile, die bewusst atomar ausgeführt werden müssen, benötigen zwingend einen Programmblock und eine Synchronisationsvariable. Und das heißt für uns Entwickler, dass wir einen vorher einfachen Block durch wait() und notify() ersetzen müssen, der synchronisiert ist. Und wir müssen uns um eine Variable kümmern. Das ist unangenehm, und wir wünschen uns ein einfacheres Konzept, so dass eine Umstellung leicht ist. Hier bieten sich Funktionsaufrufe an. Es ist schön, die Wartesituation hinter einem Paar von Funktionen wie enter() und leave() zu verstecken.
Die Idee für diese Realisierung kommt von dem niederländischen1 Informatiker Edsger Wybe Dijkstra2 . Neben vielen anderen Problemen aus der Informatik beschäftigte er sich mit der Wahl der kürzesten Wege und mit der Synchronisation von Prozessen. Zur damaligen Zeit wurde Parallelität noch durch Variablen und Warteschleifen realisiert, Programmiersprachen mit höheren Konzepten, wie sie Java bietet, waren nicht kommerziell verbreitet. Dijkstra schlug einen Satz von Funktionen P() und V() vor, die das Eintreten und Verlassen in und aus einem atomaren Block umsetzen. Dijkstra assoziierte mit den Funktionsnamen die Wörter pass und vrij, was auf Niederländisch frei heißt. Er nahm zur Verdeutlichung ein Beispiel aus dem Eisenbahnverkehr. Dort darf sich nur ein Zug auf einem Streckenabschnitt befinden, und wenn ein weiterer Zug einfahren will, so muss er warten. Er kann dann weiterfahren, wenn der erste Zug die Strecke verlassen hat. Wir erkennen hier sofort einen kritischen Abschnitt wieder, den wir in Java mit synchronized schützen würden. Da für uns P() und V() nicht so intuitiv ist und wir keine Eisenbahner sind, verwenden wir für das Eintreten die Methode enter() und für das Austreten leave(). Verglichen mit einer Sperre, hätten wir die Funktionen auch lock() und unlock() nennen können.
Der Datentyp, der diese beiden Funktionen jetzt implementiert, nennt sich Semaphore. In ihm ist intern auch noch die Synchronisationsvariable versteckt, doch dies bleibt für uns als Nutzer natürlich unsichtbar. Die Klasse lässt sich in Java leicht realisieren.
Listing 9.15 Semaphore.java
public class Semaphore { private int cnt = 1; public Semaphore( int cnt ) { if ( cnt > 1 ) // nur Zähler größer 1 akzeptieren this.cnt = cnt; } public synchronized void enter() // P { while ( cnt <= 0 ) try { wait(); } catch( InterruptedException e ) {} cnt--; } public synchronized void leave() // V { cnt++; // if ( cnt > 0 ) notify(); } }
Um zu kontrollieren, wie viele Threads auf ein Programmstück zugreifen, wird die Semaphore verwendet. Unterschieden werden zwei Arten:
| Binäre Semaphoren lassen höchstens einen Thread auf ein Programmstück zu. |
| Allgemeine Semaphoren lassen eine bestimmte begrenzte Menge an Threads in einen kritischen Abschnitt. |
Eine binäre Semaphore wird mit dem klassischem wait() und notify() realisiert. Allgemeine Semaphoren vereinfachen das Konsumenten-Produzenten-Problem. Die verbleibende Größe des Puffers ist damit auch automatisch die maximale Anzahl von Produzenten, die sich parallel im Einfügeblock befinden können.
Die Klasse java.util.concurrent.Semaphore seit Version 5 implementiert das Konzept einer Semaphore. Im Konstruktor bestimmt eine Ganzzahl die Anzahl Threads, die einen Programmteil ausführen können. Der zweite Parameter im Konstruktor gibt an, ob der am längsten Wartende der nächste ist, oder nicht. Das wird Fairness genannt.
1 Holland ist im Übrigen nur eine Provinz der Niederlande.
2 Einige Infos über ihn unter http://henson.cc.kzoo.edu/~k98mn01/dijkstra.html.
| << zurück |
Copyright © Galileo Press GmbH 2004
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.