![]() |
|
|
Warten mit await() und Aufwecken mit signal()Damit das Warten und Benachrichtigen funktioniert, kommunizieren die Parteien über ein gemeinsames Condition-Objekt, das vom Lock erfragt wird. Condition condition = lock.newCondition(); In einem fiktiven Szenario soll ein Thread T1 auf ein Signal warten und ein Thread T2 dieses Signal geben. Da nun beide Threads Zugriff auf das gemeinsame Condition-Objekt haben, kann T1 sich mit folgender Anweisung in den Schlaf begeben: try { condition. await (); } catch ( InterruptedException e ) { ... } Mit dem await() geht der Thread in den Zustand nicht ausführend über. Der Grund für den try/catch-Block ist, dass ein await() durch eine InterruptedException vorzeitig abgebrochen werden kann. Das passiert zum Beispiel, wenn der wartende Thread per interrupt()-Methode ein Hinweis zum Abbruch bekommt. Die Methode await() bestimmt den ersten Teil des Paares. Der zweite Thread T2 kann nun nach Eintreffen einer Bedingung das Signal geben: condition.signal(); Um das signal() muss es keinen eine Exception auffangenden Block geben.
Vor der Condition kommt ein LockAuf den ersten Blick scheint es, als ob das Lock-Objekt nur die Aufgabe hat, ein Condition-Objekt herzugeben. Das ist aber noch nicht alles, denn die Methoden await() und auch signal() können nur dann aufgerufen werden, wenn vorher ein lock() den Signal-Block exklusiv sperrt. lock.lock(); try { condition. await (); } catch ( InterruptedException e ) { ... } finally { lock.unlock(); } Doch was passiert ohne Aufruf von lock()? Zwei Zeilen zeigen die Auswirkung: Listing 9.22 AwaitButNoLock.java, main() Condition condition = new ReentrantLock().newCondition(); condition.await(); // java.lang.IllegalMonitorStateException Das Ergebnis ist eine java.lang.IllegalMonitorStateException. Temporäre Lock-Freigabe bei await()Um auf den Condition-Objekten also await() und signal() aufrufen zu können, ist ein vorangehender Lock nötig. Doch Moment: Wenn ein await() kommt, hält der Thread doch den Monitor, und kein anderer Thread könnte in einen kritischen Abschnitt, der über das gleiche Lock-Objekt gesperrt ist, und signal() aufrufen. Wie ist das möglich? Die Lösung besteht darin, dass await() den Monitor freigibt und den Threads so lange sperrt, bis zum Beispiel von einem anderen Thread das signal() kommt. (Wenn wir ein Programm mit nur einem Thread haben, dann ergibt natürlich so ein Pärchen keinen Sinn.) Kommt das Signal, weckt das den wartenden Thread wieder auf, und er kann am Scheduling wieder teilnehmen. Mehrere Wartende und signalAll()Es kann durchaus vorkommen, dass mehrere Threads in einer Warteposition an demselben Objekt sind und aufgeweckt werden wollen. signal() wählt dann aus der Liste der Wartenden einen Thread aus und gibt ihm das Signal. Sollten alle Wartenden einen Hinweis bekommen, lässt sich signalAll() verwenden.
wait() mit einer ZeitspanneEin await() wartet im schlechtesten Fall bis zum Nimmerleinstag, wenn es kein signal() gibt. Es gibt jedoch Situationen, in denen wir eine bestimmte Zeit lang warten, aber bei Fehlen der Benachrichtigung weitermachen wollen. Dazu kann dem await() in unterschiedlichen Formen eine Zeit mitgegeben werden.
An den Methoden ist schon zu erkennen, dass die Wartezeit einmal relativ (await()) und einmal absolut (awaitUntil()) sein kann. (Mit den eingebauten Methoden wait() und notify() ist immer nur eine relative Angabe möglich.) Eine IllegalMonitorStateException wird das Ergebnis sein, wenn beim Aufruf einer Condition-Methode das lock() des zugrunde liegenden Lock-Objekts fehlte.
9.6.2 Beispiel Erzeuger-Verbraucher-Programm
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class java.lang.Object |
| void wait() throws InterruptedException Der aktuelle Thread wartet an dem aufrufenden Objekt darauf, dass er nach einem notify()/notifyAll() weiterarbeiten kann. Der aktive Thread muss natürlich den Monitor des Objekts belegt haben. Andernfalls kommt es zu einer IllegalMonitorStateException. |
| void wait( long timeout ) throws InterruptedException Wartet auf ein notify()/notifyAll() eine gegebene Anzahl von Millisekunden. Nach Ablauf dieser 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. |
| void notify() Weckt einen beliebigen Thread auf, der an diesem Objekt wartet. |
| void notifyAll() Benachrichtigt alle Threads, die auf dieses Objekt warten. |
Ein wait() kann mit einer InterruptedException vorzeitig abbrechen, wenn der wartende Thread per interrupt()-Methode unterbrochen wird. Die Tatsache, dass wait() temporär den Lock freigibt, was für uns mit synchronized aber nicht möglich ist, spricht dafür, dass etwas wie wait() nativ implementiert werden muss.
Wenn wait() oder notify() aufgerufen werden, uns aber der entsprechende Lock für das Objekt fehlt, kommt es zum Laufzeitfehler IllegalMonitorStateException, wie wir es schon bei Condition und dem fehlenden lock() vom Lock gesehen haben.
Was wird bei folgendem Programm passieren?
Listing 9.24 NotOwner.java
class NotOwner { public static void main( String[] args ) throws InterruptedException { new NotOwner().wait(); } }
Der Compiler kann das Programm übersetzen, doch zur Laufzeit wird es zu einem Fehler kommen:
java.lang.IllegalMonitorStateException: current thread not owner at java.lang.Object.wait(Native Method) at java.lang.Object.wait(Object.java:426) at NotOwner.main(NotOwner.java:5) Exception in thread "main"
Der Fehler zeigt an, dass der aktuelle ausführende Thread (current thread) nicht den nötigen Lock besitzt, um wait() auszuführen. Das Problem ist hier mit einem synchronized-Block (oder Methode) zu lösen. Um den Fehler zu beheben, setzen wir:
NotOwner o = new NotOwner(); synchronized( o ) { o.wait(); }
Das zeigt, dass das Objekt o, das den Lock besitzt, für ein wait() »bereit« sein muss. In die richtige Stimmung wird es nur mit synchronized gebracht.
synchronized( NotOwner.class ) { new NotOwner().wait(); }
Doch natürlich könnten wir auch am Klassenobjekt synchronisieren:
synchronized( NotOwner.class ) { NotOwner.class.wait(); }
| Beispiel Die Ähnlichkeit zwischen Lock auf der einen Seite und einem synchronisierten Block bzw. Methode auf der anderen und den Methoden wait() und notify() bei Object und den analogen Methoden await() und signal() bei den Condition-Objekten ist nicht zu übersehen. Auch der Fehler beim Fehlen des Monitors ist der gleiche: ein Aufruf der Methoden await()/wait() und notify()/signal() führt zu einer IllegalMonitorStateException. Es muss also erst ein synchronisierter Block für den Monitor her oder ein Aufruf lock() auf dem Condition zugrunde liegenden Lock-Objekt. |
Synchronisationsprobleme können mittels kritischer Abschnitte und Wartesituationen mit wait() und notify() gelöst werden. Dennoch ist der eingebaute Mechanismus auch mit Nachteilen verbunden. Die große Schwierigkeit ist es, synchronisierende Programme zu entwickeln und zu warten, denn 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, sodass 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 stammt vom niederländischen1 Informatiker Edsger Wybe Dijkstra2 . Neben vielen anderen Problemen 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 mit Hilfe von Variablen und Warteschleifen realisiert; Programmiersprachen mit höheren Konzepten, wie sie Java bietet, waren nicht 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 aufhalten, wenn ein zweiter Zug einfahren will, muss er warten und kann nur weiterfahren, wenn der erste Zug die Strecke verlassen hat.
Um zu kontrollieren, wie viele Threads auf ein Programmstück zugreifen können, kann in Java die Klasse java.util.concurrent.Semaphore verwendet werden. Mit ihr lassen sich zwei Typen von Semaphoren umsetzen:
| 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. Die Semaphore verwaltet intern eine Menge so genannter Erlaubnisse (eng. permits). |
Eine binäre Semaphore wird mit dem klassischem wait() und notify() realisiert. Allgemeine Semaphoren vereinfachen das Konsumenten-Produzenten-Problem, da eine bestimmte Anzahl von Threads in einem Block erlaubt sind. Die verbleibende Größe des Puffers ist somit automatisch die maximale Anzahl von Produzenten, die sich parallel im Einfügeblock befinden können.
Die wichtigen Eigenschaften der Semaphore-Klasse sind der Konstruktor und die Methoden zum Betreten und Verlassen des kritischen Abschnitts.
| class java.util.concurrent.Semaphore implements Serializable |
| Semaphore( int permits ) Eine neue Semaphore, die bestimmt, wie viele Threads in einem Block sein dürfen. |
| void acquire() Versucht, in den kritischen Block einzutreten. Wenn der gerade belegt ist, wird gewartet. Vermindert die Menge der Erlaubnisse um eins. |
| void release() Verlässt den kritischen Abschnitt und legt eine Erlaubnis zurück. |
Unser Beispiel soll mit einer Semaphore arbeiten, die nur zwei Threads gleichzeitig in den kritischen Abschnitt lässt.
Listing 9.25 SemaphoreDemo.java
import java.util.concurrent.Semaphore; public class SemaphoreDemo { static Semaphore semaphore = new Semaphore( 2 );
Der kritische Abschnitt besteht aus zwei Operationen: eine Ausgabe auf dem Bildschirm und eine Wartezeit von zwei Sekunden. Er ist in einem Runnable eingebettet:
static Runnable r = new Runnable() { public void run() { while ( true ) { try { semaphore.acquire(); System.out.println( "Thread=" + Thread.currentThread().getName() + ", Available Permits=" + semaphore.availablePermits() ); Thread.sleep( 2000 ); } catch ( InterruptedException e ) { e.printStackTrace(); } finally { semaphore.release(); } } } };
Der kritische Abschnitt beginnt mit dem acquire() und endet mit release() im finally. In der Ereignisbehandlung fangen wir eine mögliche InterruptedException von acquire() und Thread.sleep() auf. Das release() ist im finally sehr gut aufgehoben, denn wir wollen in jedem Fall, auch wenn irgendwie eine andere RuntimeException auftauchen sollte, den Lock wieder freigeben.
Im letzten Teil starten wir einfach drei Threads:
public static void main( String[] args ) { new Thread( r ).start(); new Thread( r ).start(); new Thread( r ).start(); } }
Nach dem Starten ist gut zu beobachten, wie jeweils zwei Threads im Abschnitt sind (eine Leerzeile symbolisiert die Wartezeit):
Thread=Thread-0, Available Permits=1 Thread=Thread-1, Available Permits=0 Thread=Thread-2, Available Permits=0 Thread=Thread-0, Available Permits=0 Thread=Thread-2, Available Permits=0 Thread=Thread-0, Available Permits=0
In der Ausgabe ist zu sehen, dass Thread 0, 1 und 2 zwar ihre Aufgaben ausführen können, aber plötzlich eine Sequenz 0, 2, 0 entsteht. Unser Gerechtigkeitssinn sagt uns jedoch, dass Thread 1 wieder an die Reihe kommen müsste. Wie ist das möglich? Die Antwort lautet, dass das acquire() nicht berücksichtigt, wer am längsten wartet, sondern dass es sich aus der Liste der Wartenden einen beliebigen Thread auswählt. (Wir kennen das von notify() her und dem Betreten eines synchronized Blocks.) Um ein faires Verhalten zu realisieren, wird die Fairness einfach über den Konstruktor von Semaphore angegeben. Ändern wir im Programm folgende Zeile:
static Semaphore semaphore = new Semaphore( 2, true );
Nun bekommen wir folgenden Ausgabe:
Thread=Thread-0, Available Permits=1 Thread=Thread-1, Available Permits=0 Thread=Thread-2, Available Permits=0 Thread=Thread-0, Available Permits=0 Thread=Thread-1, Available Permits=0 Thread=Thread-2, Available Permits=0 Thread=Thread-0, Available Permits=0 Thread=Thread-1, Available Permits=0
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 2005
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.