Java Best Practice (Teil VIII) Concurrency - Lästige Konkurrenz (I) Ob bei der Server-Entwicklung mit EJB oder der Implementierung einer Swing- Anwendung: Parallele Ausführungsprozesse sind allgegenwärtig. Programmierrichtlinien sorgen dafür, dass der Entwickler sich um das Thema Multi-Threading keine Gedanken machen muss. Dennoch lohnt ein Blick hinter die Kulissen. Einerseits werden Programmierrichtlinien verständlicher, andererseits lassen sich Risiken einer Multi-Threading-Entwicklung besser einschätzen. Wieso parallel? Unter einem Prozess wird in diesem Artikel ein Ausführungsstrang (Thread, Kontrollfluss) innerhalb eines Java-Programms verstanden. Beim Multi-Threading gibt es mehrere solcher Ausführungsstränge, die parallel zueinander arbeiten und dabei auf einen gemeinsamen Speicherbereich zugreifen. Parallele Prozesse werden eingesetzt, um die Wirkung einer parallelen Verarbeitung zu erzielen. Ein offensichtliches Beispiel ist die GUI, die während des Bildaufbaus Benutzereingaben zulässt. Für Web- oder Application-Server werden Threads eingesetzt, um parallel eine Vielzahl von Anfragen zu bearbeitet. Wo ist das Problem? Die Entwicklung von nebenläufigen Prozessen ist dann risikoreich, wenn parallel auf Objekte zugegriffen wird (Concurrency). Dabei werden mehrere Objektmethoden gleichzeitig ausgeführt, die Zustandsänderung ist schwer vorhersagbar. Im Umgang mit Concurrency ist daher die wichtigste Regel: Vermeiden Sie parallele Zugriffe auf Objekte. Andernfalls werden Sie erstaunt sein, zu welchem Zeitpunkt Fehler auftauchen und wie viel Aufwand zur Problemlösung benötigt wird. Ein Thread ist schnell gestartet In jedem Fall ist Aufklärung hilfreich. In einem ersten Beispiel werden drei Lichter in rot, gelb und grün betrachtet. Diese werden kurz hintereinander eingeschaltet und blinken dann für eine Weile. Anschließend erlöschen sie. In Abbildung 1 ist dieser Sachverhalt in einem Aktivitätsdiagramm dargestellt. Es wird vom Startpunkt aus gelesen. Pfeile verbinden die Aktivitäten eines Kontrollflusses. Das Einschalten des roten Lichts erfolgt am horizontalen Balken. Dort entsteht aus einem Kontrollfluss ein Zweiter: Das Blinken des Lichts findet in einem unabhängigen Kontrollfluss statt. Das Einschalten der beiden anderen Lichter führt dazu, dass zwei weitere Kontrollflüsse dazu kommen. Insgesamt gibt es vier parallele Kontrollflüsse. Nachdem alle Lichter erloschen sind, vereinen sich die vier Kontrollflüsse wieder. Die Java Elemente sind in Abbildung 1 als Kommentar angegeben. Ein neuer Kontrollfluss wird durch die Methode start der Klasse Thread erzeugt. Objekte vom Typ Runnable definierten in der Methode run die Aktivitäten. Verteilung von Rechenzeit nicht vorhersehbar Abbildung 2 enthält den Quellcode. Die innere Klasse Licht ist vom Typ Runnable. Sie implementiert das Blinken dadurch, dass zwei Mal pro Sekunde der Farbname des Lichts auf der Konsole ausgegeben wird. In der Methode main wird zu jedem Licht ein Thread erzeugt. Gestartet wird der Thread – dieser führt dann die Methode run aus. Damit die Lichter bis zu ihrem Ende leuchten, muss Thread.join aufgerufen werden. Es ist bemerkenswert, dass die Ausgaben rot, gelb und grün auf der Konsole nicht streng periodisch ausgegeben werden. Ab und an überholt ein Licht das andere. Wann und wie oft ein Thread Rechenzeit bekommt ist nicht vorhersehbar! Das Ende eines Threads Mit Thread.start() beginnt die Ausführung des Threads. Zum Beenden gibt es keine vergleichbare Methode. Das Beenden eines Threads ist nur in Zusammenarbeit mit seinem Runnable-Objekt möglich. Die Methode run muss sich in Folge eines Signals beenden. Im Listing der Abbildung 3 können die Lichter unendlich lange blinken. Durch den Aufruf der Methode interrupt bekommt das Runnable-Objekt das Signal, dass die Ausführung unterbrochen werden soll. In dem Beispielprogramm wird das Signal durch das Fangen der InterruptedException ausgewertet. Mit return wird die Methode run verlassen. Alternativ kann die Runnable-Instanz an bestimmten Stellen die Methode Thread. interrupted() aufrufen, um zu erfahren, ob ihm ein Interrupt zugesendet wurde (siehe Listing in Abbildung 4). Implementierung einer Verkehrszählung Im Listing der Abbildung 4 ist der Kern eines Verkehrszählungsprogramms dargestellt. Das vollständige Skript kann auf unserer Internet-Seite heruntergeladen werden [3]. Hilfskräfte drücken für jedes Fahrzeug auf einen Knopf, so dass weiter gezählt wird. Zur weiteren Auswertung befindet sich das Zählwerk auf einem zentralen Server. Das Beispiel ist etwas vereinfacht: Die Hilfskraft wird durch einen Thread simuliert, der ununterbrochen weiter zählt. Das Zählwerk wird durch ein Singleton implementiert, dass den Zeitpunkt und die Nummer des Fahrzeugs in einer Liste notiert. Zur Berücksichtigung der Konkurrenz beim Zugriff auf die Liste wird eine Collections.SynchronizedList verwendet. Damit jeder Thread über den aktuellen Wert des Zählers verfügt, wird dieser als volatile gekennzeichnet. Der Ausschnitt der Ausgabe in Abbildung 5 zeigt, dass es beim Zählen einige Unregelmäßigkeiten gibt. So werden manche Nummern beim Zählen ausgelassen – manche werden doppelt verwendet. Woran scheitert die Verkehrszählung? Die Verkehrszählung scheitert dann, wenn mehrere Threads gleichzeitig die Methode Verkehrszaehler.weiter ausführen. Für jede Methode wird zwar ein eigener Speicherbereich für die lokalen Variablen erstellt, die Objekt-Attribute liegen dagegen nur einmalig vor. So werden diese von den verschiedenen Threads in unerwarteter Weise manipuliert. Zur Diskussion von Konkurrenzsituationen eigenen sich besonders Sequenzdiagramme. Der Vorgang counter = counter +1 in der Methode wird in Abbildung 6 weiter dargestellt. Immer dann, wenn Hilfskraft 2 sich den Wert der Variable counter holt, bevor Hilfskraft 1 sein Ergebnis geschrieben hat, erhöht sich der Wert des Counters nur um eins. Dabei wurden zwei Fahrzeuge gezählt. Das Schlüsselwort synchronized Demzufolge muss verhindert werden, dass zwei Threads gleichzeitig den Wert der Variable counter erhöhen. Java sieht dazu eine Sperre für einen Ausführungsbereich vor. Ist diese gesetzt, so warten andere Threads, dass diese Sperre wieder zurückgenommen wird, bevor sie den Ausführungsbereich betreten. In Abbildung 7 sind die Zeilen des Codes zu sehen, die für die Sperre sorgen. Um das Inkrementieren des Counters wird ein Synchronized-Block erstellt. Dazu muss ein Objekt angegeben werden, bei dem die Sperrinformation gespeichert wird. Dies ermöglicht die gleichzeitige Sperrung mehrerer Blöcke. In der Abbildung 8 ist dargestellt, wie sich der Synchronized-Block auf die Ausführung auswirkt. Das Setzen der Sperre durch die Hilfskraft 1 verhindert, dass die Hilfskraft 2 den Block betritt. Diese muss warten. Hilfskraft 2 wird beim Verlassen des Blocks informiert, dass er nun weiter arbeiten kann. Wird das Schlüsselwort synchronized vor die Methodendefinition gesetzt, also synchronized public weiter(), so umfasst die Sperre den gesamten Block der Methode. Die Sperre wird dem zugehörigen Objekt zugeordnet. Lock, Unlock, Deadlock In der Fachliteratur wird das Setzen der Sperre als lock, das Entsperren als unlock bezeichnet. Und dann gibt es noch den Deadlock. Dieser bezeichnet die Blockade, wenn zwei Threads gegenseitig auf die Freigabe eines Locks warten. In diesem Fall wird die Ausführung der beteiligten Threads unterbunden. Wie ein Deadlock zustande kommt ist der Abbildung 9 zu entnehmen. Mitspieler sind dabei zwei Objekte, die synchronisierte Ausführungsblöcke enthalten: Synchable 1 und Synchable 2. Diese Ausführungsblöcke werden von jeweils einem Thread betreten und damit verschlossen. Erfolgt aus dem synchronisierten Ausführungsblock jeweils der Aufruf eines synchronisierten Blocks des anderen Objekts, so ist der Deadlock perfekt. Die beiden Threads verharren auf ewig im Wartezustand. Der Einwand - so etwas programmiert man doch nicht - ist korrekt. Solche Konstellationen ergeben sich von ganz allein. Derartige Probleme werden erst beim Lasttest der Anwendung festgestellt – wenn nicht gar erst im Betrieb. Existiert die Möglichkeit eines Deadlocks, so laufen mehr und mehr Threads in diese Falle. Ressourcen werden nicht freigegeben. Die Anwendung wird langsam oder bricht beim Versuch ab, weitere Ressourcen zu bekommen. Fazit Das Fazit ist der erhobene Zeigefinger: Achtung, Multi-Threading! Auch wenn grundlegende Syntaxelemente für den Umgang mit parallelen Prozessen dargestellt werden, ergibt sich daraus noch kein Rezept für die problemlose Verwendung. In den folgenden Artikeln dieser Serie wird daher der Aspekt des Multi-Threadings in unterschiedlichen Situationen näher untersucht und Lösungsansätze werden vorgestellt. Wer bis dahin nicht warten kann, sollte zunächst einen Blick auf das Paket java.util.concurrent werfen. Hier finden sich Klassen für viele Standardsituationen. Übrigens: Die Grundlagen der Thread- Programmierung können Sie auch in unserem Seminar Java Programmierung Grundlagen erlernen. Gern stehen wir Ihnen in Entwicklungsprojekten mit unserer Erfahrung zur Seite. Dr. Stefan Koch (info@ordix.de). Links [1] Kapitel Concurrency im Java-Tutorial: ..http://download.oracle.com/javase/tutorial/essential/concurrency/index.html [2] Seminarempfehlung „Java Programmierung Grundlagen“: ..http://www.ordix.de/trainingsshop/siteengine/action/load/kategorie/Java-JEE/nr/68/index.html [3] Ausführliches Listing des Verkehrszählungsprogramms: ..http://www.ordix.de/ORDIXNews/verzeichnis_42010.html Glossar Thread Elementaraufgabe. Die Befehle eines Threads sind in sich so abgeschlossen, dass sie auf einer CPU zusammenhängend ausgeführt werden können. Um Programme mehrprozessorfähig zu gestalten, müssen die Abläufe in Threads untergliedert sein. Multi-Threading Multi-Threading bedeutet, dass Programme parallel verarbeitet werden können. Concurrency Konkurrierender Zugriff auf Objektattribute.