Nebenläufige Programmierung mit dem java.util.concurrent-Paket in Java 1.5

Status quo vadis?

Oliver Zeigermann

Bei der Implementierung nebenläufiger Systeme ist eine gehörige Portion Kunst notwendig. Es gibt immer noch genug Möglichkeiten, Fehler zu machen; man muss sich immer noch um das Wie, nicht nur um das Was kümmern. Um stabilere Programme herzustellen, ist es jedoch notwenig, den Anteil der Kunst möglichst gering zu halten. Eben dabei hilft uns das java.util.concurrent-Paket. Durch seine Vorgaben bringt es Klarheit in die gebräuchlichsten Problemstellungen der nebenläufigen Programmierung und liefert die komplexesten und mühsamsten Implementierungen gleich dazu, was der folgende Artikel verdeutlichen soll.

Java ist eine Programmiersprache, die das Erstellen von nebenläufigen Programmen, d.h. von Programmen, deren Ausführung auf mehrere Prozesse verteilt ist, ausdrücklich unterstützt. Das Prozesskonzept wird als Teil der Sprache angesehen und durch so genannte Threads realisiert. Zur Gewährleistung der Korrektheit von nebenläufigen Programmen kommt bei Java das Monitorkonzept zur Anwendung. Dabei wacht ein Objekt, der so genannte Monitor, darüber, dass höchstens ein Thread zur Zeit ein bestimmtes Code-Fragment – einen so genannten kritischen Abschnitt – ausführen kann. Dies ist z.B. für das Testen und darauf aufbauende Umsetzen von Variablen notwendig. Zudem gibt es die Möglichkeit, die Ausführung eines Threads innerhalb eines kritischen Abschnitts zu unterbrechen. Dies erlaubt anderen Threads – wahlweise nur für einige Zeit -, diesen oder einen anderen von demselben Monitor geschützten kritischen Abschnitt zu betreten. Nachdem diese anderen Threads ihre Arbeit getan haben, muss der wartende Thread davon in Kenntnis gesetzt werden, sodass er im kritischen Abschnitt fortfahren kann. In der Praxis kann ein Thread innerhalb des kritischen Abschnitts ungestört prüfen, ob bestimmte Bedingungen eingetreten sind. Ist dies nicht der Fall, so wartet er und gibt einem anderen Thread die Möglichkeit, für besagte Bedingungen zu sorgen. Diese Bedingungen können z.B. das Vorliegen von Daten für den Datenaustausch oder das Eintreten von bestimmten anderen Zuständen sein.

Kritische Abschnitte werden in Java durch Synchronized-Blöcke über gewöhnliche Objekte, die auch als Monitore dienen, definiert. Die Methoden zum Warten innerhalb von kritischen Abschnitten und innerhalb der Benachrichtigungen von wartenden Threads werden darum in der Klasse Object zur Verfügung gestellt. Die entsprechenden Methoden heißen für das Warten wait(), wait(long timeout) und wait(long timeout, int nanos) und für die Benachrichtigung notify() bzw. notifyAll().

java.util.concurrent

Die Programmierung nebenläufiger Programme ist ein fehlerträchtiges und kompliziertes Unterfangen. Zum einen gibt es Probleme durch Ungenauigkeiten [1] in der Java-Spezifikation, zum anderen ist es eher eine Kunst als ein Handwerk, Code-Abschnitte im richtigen Maß und an richtiger Stelle zu schützen. Mangelnder Schutz macht sich in der Praxis durch inkonsistente Daten, übermäßiger Schutz im besten Fall durch unnötig eingeschränkte Nebenläufigkeit, d.h. die Anzahl gleichzeitig lauffähiger Threads ist unnötig, beschränkt, im schlechtesten Fall durch Verklemmungen bemerkbar. Verklemmungen oder auch Deadlocks nennt man blockierte Programmzustände, in denen zwei Threads gegenseitig aufeinander warten. Da beide Threads blockiert sind, ist dieser Zustand nicht von den beteiligten Threads aufzulösen. Schutz an falscher Stelle ist als Kombination von wirkungslosem und übermäßigem Schutz nicht nur der unglücklichste, sondern auch der häufigste Fehlerfall.

In Java 1.5 wurden durch die Java Specification Requests 133 und 166 Ungenauigkeiten in der Java-Spezifikation ausgeräumt und mit dem Paket java.util.concurrent Hilfsklassen für die gängigsten Problemstellungen der nebenläufigen Programmierung zur Verfügung gestellt. Das aus [2] hervorgegangene Paket umfasst unter anderem Lösungen aus den Bereichen rudimentäre atomare Operationen, Sperren, Warteschlangen, Synchronisation und nebenläufige Collections. Hinzu kommen noch einige Änderungen außerhalb des java.util.concurrent-Pakets. Dazu gehören unter anderem eine Erweiterung des Java Collections Framework um das Interface Queue und Anpassungen für ein flexibles Thread Pooling. Generell bieten alle wartenden Methoden eine feine Auflösung ihrer Wartezeit bis hin in den Nanosekunden-Bereich. Dies wird durch die Erweiterung der Systemklasse um die Methode nanoTime() und die Klasse TimeUnit zur Repräsentation und Konvertierung von Zeiteinheiten unterstützt.

Locks in java.util.concurrent.locks

Die im Paket java.util.concurrent.locks enthaltenen Klassen dienen als flexiblerer Ersatz für das eingebaute Monitorkonzept. Anstatt von synchronized-Blöcken werden mit der Klasse ReentrantLock als Implementierung des Lock-Interfaces kritische Abschnitte eingeklammert. Komplementär dazu werden über Implementierungen des Condition-Interfaces die Methoden wait und notify durch await und signal ersetzt. Dabei erzeugt das Lock-Objekt die zugehörigen Condition-Objekte und stellt so eine Assoziation zwischen diesen her.

Ergänzend existiert die sehr nützliche Klasse ReentrantReadWriteLock, die das Interface ReadWriteLock implementiert und die Unterscheidung von exklusivem und gemeinsamem Zugriff auf kritischen Abschnitten zulässt. Schließlich stehen mit der Klasse LockSupport systemnahe Methoden zur Implementierung eigener Lock-Klassen mit eigenen Fairness- und Prioritätsstrategien zur Verfügung.

Executors in java.util.concurrent

Die größte Anzahl von Klassen im java.util.concurrent-Paket sind der flexiblen Ausführung von Programmteilen gewidmet. Mittels des Interfaces Executor werden von der Klasse Thread als ausführende Instanz Runnables abstrahiert. Auf welche Weise das Runnable ausgeführt wird, ist erst durch die implementierende Klasse festgelegt. Für die Ausführung durch Threads aus einem Pool stellt dieses Paket die Klasse ThreadPoolExecutor zur Verfügung.

Sollen Daten asynchron im Hintergrund, d.h., ohne dass man blockierend darauf wartet, berechnet werden, hilft das Interface Future. Es wird nur blockiert, wenn die zu berechnenden Daten zum Zeitpunkt des tatsächlichen Zugriffs noch nicht verfügbar sind. In der Implementierung FutureTask, die ebenfalls das Interface Cancellable für den vorzeitigen Abbruch implementiert, können Aufgaben vom Typ Callable im Hintergrund ausgeführt werden. Das Interface Callable erweitert das Runnable-Interface derart, dass ein Wert zurückgegeben und im Fehlerfall eine Exception geworfen werden kann. In der endgültigen Version werden Future und Cancellable in einem Interface zusammengefasst, um die Arbeit mit den JSRs 236 [3] und 237 [4] zu vereinfachen.

Synchronizers in java.util.concurrent

Semaphoren dienen zur Beschränkung der Anzahl von Threads, die gleichzeitig auf eine Ressource zugreifen können. Beschränkt man die Anzahl der erlaubten Threads auf eins, so erhält man eine binäre Semaphore für den wechselseitigen Ausschluss zweier Threads. Im java.util.concurrent-Paket wird dieser Mechanismus mit der gleichnamigen Klasse Semaphore bereitgestellt. Die Klasse FairSemaphore garantiert zusätzlich, dass wartende Threads in der Reihenfolge bedient werden, in der sie um Erlaubnis für den Zugriff baten.

Zudem gibt es einige weitere Synchronisations-Hilfen, die durch Rendezvous-Verfahren entweder Daten austauschen (Exchanger) oder innehalten, bis alle zur Verabredung erwarteten Threads ankommen, (CyclicBarrier) oder auf einen von außen gegebenen Startschuss warten (CountdownLatch). Diese Methoden sind besonders in Initialisierungsphasen und bei der verteilten Verarbeitung und Berechnungen von Daten nützlich.

Fazit

Als Ausblick hat Doug Lea, der geistige Vater des Pakets, bereits Arbeiten an Klassen für transaktionale Systeme mit zugehörigen Sperren auf höherer Ebene angekündigt. Dadurch würde die Implementierung eigener transaktionaler Systeme, wie z.B. Datenbanken, erleichtert. Vielleicht noch wichtiger, erlaubt ein solches Paket auch für alltägliche Aufgaben eine weitere Verringerung der Kunst und einen Programmierkomfort in Bezug auf Nebenläufigkeit, wie er bisher nur in J2EE-Umgebungen zu finden war.

Links und Literatur

Geschrieben von
Oliver Zeigermann
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.