Galileo Computing < openbook >
Galileo Computing - Professionelle Buecher. Auch fuer Einsteiger.
Galileo Computing - Professionelle Buecher. Auch fuer Einsteiger.


Java ist auch eine Insel von Christian Ullenboom
Buch: Java ist auch eine Insel (Galileo Computing)
gp Kapitel 9 Threads und nebenläufige Programmierung
gp 9.1 Prozesse und Threads
gp 9.1.1 Wie parallele Programme die Geschwindigkeit steigern können
gp 9.2 Threads erzeugen
gp 9.2.1 Threads über die Schnittstelle Runnable implementieren
gp 9.2.2 Threads über Runnable starten
gp 9.2.3 Die Klasse Thread erweitern
gp 9.2.4 Erweitern von Thread oder Implementieren von Runnable?
gp 9.3 Threads schlafen
gp 9.3.1 Eine Zeituhr
gp 9.4 Die Klassen Timer und TimerTask
gp 9.5 Die Zustände eines Threads
gp 9.5.1 Das Ende eines Threads
gp 9.5.2 Einen Thread höflich mit Interrupt beenden
gp 9.5.3 Der stop() von außen
gp 9.5.4 Das ThreadDeath-Objekt
gp 9.5.5 Auf das Ende warten mit join()
gp 9.6 Arbeit niederlegen und wieder aufnehmen
gp 9.7 Priorität
gp 9.7.1 Threads hoher Priorität und das AWT
gp 9.7.2 Granularität und Vorrang
gp 9.8 Dämonen
gp 9.9 Kooperative und nichtkooperative Threads
gp 9.10 Synchronisation über kritische Abschnitte
gp 9.10.1 Gemeinsam genutzte Daten
gp 9.10.2 Probleme beim gemeinsamen Zugriff und kritische Abschnitte
gp 9.10.3 Punkte parallel initialisieren
gp 9.10.4 i++ sieht atomar aus, ist es aber nicht
gp 9.10.5 Abschnitte mit synchronized schützen
gp 9.10.6 Monitore
gp 9.10.7 Synchronized-Methode am Beispiel der Klasse StringBuffer
gp 9.10.8 Synchronisierte Blöcke
gp 9.10.9 Vor- und Nachteile von synchronisierten Blöcken und Methoden
gp 9.10.10 Nachträglich synchronisieren
gp 9.10.11 Monitore sind reentrant, gut für die Geschwindigkeit
gp 9.10.12 Deadlocks
gp 9.10.13 Erkennen von Deadlocks
gp 9.11 Variablen mit volatile kennzeichnen
gp 9.12 Synchronisation über Warten und Benachrichtigen
gp 9.12.1 Falls der Lock fehlt: IllegalMonitorStateException
gp 9.12.2 Warten mit wait() und Aufwecken mit notify()
gp 9.12.3 Mehrere Wartende und notifyAll()
gp 9.12.4 wait() mit einer Zeitspanne
gp 9.12.5 Beispiel Erzeuger-Verbraucher-Programm
gp 9.12.6 Semaphoren
gp 9.12.7 Die Concurrency Utilities von Doug Lea
gp 9.13 Aktive Threads in der Umgebung
gp 9.14 Gruppen von Threads in einer Thread-Gruppe
gp 9.14.1 Etwas über die aktuelle Thread-Gruppe herausfinden
gp 9.14.2 Threads in einer Thread-Gruppe anlegen
gp 9.14.3 Methoden von Thread und ThreadGroup im Vergleich
gp 9.15 Einen Abbruch der virtuellen Maschine erkennen

Kapitel 9 Threads und nebenläufige Programmierung

Just Be.
- Calvin Klein


Galileo Computing

9.1 Prozesse und Threadsdowntop

Moderne Betriebssysteme geben dem Benutzer die Illusion, dass verschiedene Programme gleichzeitig ausgeführt werden - die Betriebssysteme nennen sich multitaskingfähig. Was wir dann wahrnehmen, ist eine Quasiparallelität, die im Deutschen auch »Nebenläufigkeit«1 genannt wird. Diese Nebenläufigkeit der Programme wird durch das Betriebssystem gewährleistet, welches auf Einprozessormaschinen die Prozesse alle paar Millisekunden umschaltet. Daher ist das Programm nicht wirklich parallel, sondern das Betriebssystem gaukelt uns dies durch verzahnte Bearbeitung der Prozesse vor. Ob wir aber nun einen oder beliebig viele kleine Männchen im Rechner arbeiten haben, soll uns egal sein.

Der Teil des Betriebssystems, der die Prozessumschaltung übernimmt, heißt Scheduler. Die dem Betriebssystem bekannten, aktiven Programme bestehen aus Prozessen. Ein Prozess setzt sich aus dem Programmcode und den Daten zusammen und besitzt einen eigenen Adressraum. Die virtuelle Speicherverwaltung des Betriebssystems trennt die Adressräume der einzelnen Prozesse. Dadurch ist es unmöglich, dass ein Prozess den Speicherraum eines anderen Prozesses korrumpiert; er sieht den anderen Speicherbereich nicht. Amok laufende Programme sind zwar möglich, werden jedoch vom Betriebssystem gestoppt. Zu jedem Prozess gehören des Weiteren Ressourcen wie geöffnete Dateien oder belegte Schnittstellen.

Es ist wünschenswert, Parallelität innerhalb eines einzigen Programms zur Verfügung zu haben und nicht nur innerhalb des Betriebssystems, welches ebenfalls quasiparallel ausführt. Und da ist Java eine Sprache, die nebenläufige Programmierung direkt unterstützt. Das Konzept beruht auf so genannten Threads (zu Deutsch »Faden« oder »Ausführungsstrang«). Dies sind parallel ablaufende Aktivitäten, die sehr schnell in der Umschaltung sind. Innerhalb eines Prozesses kann es mehrere Threads geben, die alle zusammen in demselben Adressraum ablaufen. Die einzelnen Threads eines Prozesses können daher untereinander auf ihre öffentlichen Daten zugreifen.

Mehrere Threads können wie Prozesse verzahnt ausgeführt werden, so dass sich für den Benutzer der Eindruck von Gleichzeitigkeit ergibt. Trifft diese Aussage zu, dann nennen wir die Umgebung, in der das Programm abläuft, auch multithreaded. Threads können vom Scheduler sehr viel schneller umgeschaltet werden als Prozesse, so dass wenig Laufzeitverlust entsteht. Unterstützt das Betriebssystem des Rechners, auf dem die JVM läuft, Threads direkt, so nutzt die Laufzeitumgebung diese Fähigkeit in der Regel. In diesem Fall haben wir es mit nativen Threads zu tun. Falls das Betriebssystem jedoch keine Threads unterstützt, wird die Parallelität von der virtuellen Maschine simuliert. Der Java-Interpreter regelt dann den Ablauf, die Synchronisation und die verzahnte Ausführung. Die Sprachdefinition lässt die Art Implementierung - also nativ oder nicht - von Threads bewusst frei. Es ist aber wahrscheinlich, dass threadunterstützende Betriebssysteme die Thread-Verwaltung auch nativ umsetzen. Das ist jedoch nicht immer ideal: Native Threads haben einen höheren konstanten Speicher-Overhead.

Es stellt sich die Frage, warum denn nicht alle Laufzeitumgebungen die Threads auf das Betriebssystem abbilden. Dann wäre die JVM entlastet und die Verteilung auch auf Mehrprozessorsystemen geregelt. In diesem Fall brächte das aber den Nachteil mit sich, dass das Betriebssystem in den Threads auch Bibliotheksaufrufe ausführen kann, zum Beispiel, um das Eingabe- und Ausgabesystem zu verwenden oder um grafische Ausgaben zu machen. Damit das aber ohne Probleme funktioniert, müssen diese Bibliotheken threadsicher sein. Da hatten die Unix-Versionen jedoch diverse Probleme, insbesondere die grafische Standardbibliothek X11 und Motif waren lange nicht threadsicher. Um schwer wiegenden Problemen mit grafischen Oberflächen aus dem Weg zu gehen, haben die Entwickler daher auf eine native Multithreaded-Umgebung zunächst verzichtet.

Mittlerweile unterstützen viele Betriebssysteme POSIX-Threads, und auch in C(++) wird paralleles Programmieren durch passende Bibliotheken populär. Doch besonders die Integration in die Sprache macht das Entwerfen nebenläufiger Anwendungen in Java einfacher. Durch die verzahnte Ausführung kommt es allerdings zu Problemen, die Datenbankfreunde von Transaktionen kennen. Es besteht die Gefahr konkurrierender Zugriffe auf gemeinsam genutzte Ressourcen. Um dies zu vermeiden, kann der Programmierer durch synchronisierte Programmblöcke gegenseitigen Ausschluss sicherstellen. Damit steigt aber auch die Gefahr für Verklemmungen (engl. deadlocks)


Galileo Computing

9.1.1 Wie parallele Programme die Geschwindigkeit steigern könnentoptop

Auf den ersten Blick ist es nicht ersichtlich, warum auf einem Einprozessorsystem die nebenläufige Abarbeitung eines Programms geschwindigkeitssteigernd sein kann. Betrachten wir daher ein Programm, welches eine Folge von Anweisungen ausführt. Die Programmsequenz dient zum Visualisieren eines Datenbank-Reports. Zunächst wird ein Fenster zur Fortschrittsanzeige dargestellt. Anschließend werden die Daten analysiert, und der Fortschrittsbalken wird kontinuierlich aktualisiert. Schließlich werden die Ergebnisse in eine Datei geschrieben. Die Schritte sind:

1. Baue Fenster auf.
2. Öffne Datenbank vom Netz-Server und lese Datensätze. 3. Analysiere Daten und visualisiere den Fortschritt. 4. Öffne Datei und schreibe erstellten Report.

Was auf den ersten Blick wie ein typisches sequenzielles Programm aussieht, kann durch geschickte Parallelisierung beschleunigt werden.

Zum Verständnis ziehen wir noch einmal den Vergleich zu Prozessen. Nehmen wir an, auf einer Einprozessormaschine sind fünf Benutzer angemeldet, die im Editor Quelltext tippen und hin und wieder den Java-Compiler bemühen. Die Benutzer würden vermutlich die Belastung des Systems durch die anderen nicht mitbekommen, denn Editoroperationen lasten den Prozessor nicht aus. Wenn Dateien kompiliert und somit vom Hintergrundspeicher in den Hauptspeicher transferiert werden, ist der Prozessor schon besser ausgelastet, doch dies geschieht nicht regelmäßig. Im Idealfall übersetzen alle Benutzer nur dann, wenn die anderen gerade nicht übersetzen - im schlechtesten Fall möchten natürlich alle Benutzer gleichzeitig übersetzen.

Übertragen wir die Verteilung auf unser Problem, nämlich wie der Datenbank-Report schneller zusammengestellt werden kann. Beginnen wir mit der Überlegung, welche Operationen parallel ausgeführt werden können.

gp Das Öffnen von Fenster, Ausgabedatei und Datenbank kann parallel geschehen.
gp Das Lesen von neuen Datensätzen und das Analysieren von alten Daten kann gleichzeitig ablaufen.
gp Alte analysierte Werte können während der neuen Analyse in die Datei geschrieben werden.

Wenn die Operationen wirklich parallel ausgeführt werden, kann bei Mehrprozessorsystemen ein enormer Leistungszuwachs verzeichnet werden. Doch interessanterweise ergibt sich dieser auch bei nur einem Prozessor, was in den Aufgaben begründet liegt. Denn bei den gleichzeitig auszuführenden Aufgaben handelt es sich um unterschiedliche Ressourcen. Wenn die grafische Oberfläche das Fenster aufbaut, braucht sie dazu natürlich Rechenzeit. Parallel kann die Datei geöffnet werden, wobei weniger Prozessorleistung gefragt ist als vielmehr die vergleichsweise träge Festplatte angesprochen wird. Das Öffnen der Datenbank wird auf den Datenbank-Server im Netzwerk abgewälzt. Die Geschwindigkeit hängt von der Belastung des Servers und des Netzes ab. Wenn anschließend die Daten gelesen werden, muss die Verbindung zum Datenbank-Server natürlich stehen. Daher sollten wir zuerst die Verbindung aufbauen.

Ist die Verbindung hergestellt, lassen sich über das Netzwerk Daten in einen Puffer holen. Der Prozessor wird nicht belastet, vielmehr der Server auf der Gegenseite und das Netzwerk. Während der Prozessor also vor sich hindöst und sich langweilt, können wir ihn derweil besser beschäftigen, indem er alte Daten analysiert. Wir verwenden hierfür zwei Puffer. In den einen lädt ein Thread die Daten, während ein zweiter Thread die Daten im anderen Puffer analysiert. Dann werden die Rollen der beiden Puffer getauscht. Jetzt ist der Prozessor beschäftigt. Er ist aber vermutlich fertig, bevor die neuen Daten über das Netzwerk eingetroffen sind. In der Zwischenzeit können die Report-Daten in den Report geschrieben werden; eine Aufgabe, die wieder die Festplatte belastet und weniger den Prozessor.

Wir sehen an diesem Beispiel, dass durch hohe Parallelisierung eine Leistungssteigerung möglich ist, da die bei langsamen Operationen anfallenden Wartezeiten genutzt werden können. Langsame Arbeitsschritte lasten den Prozessor nicht aus, und die anfallende Wartezeit vom Prozessor beim Netzwerkzugriff auf eine Datenbank kann für andere Aktivitäten genutzt werden. Die Tabelle gibt die Elemente zum Kombinieren noch einmal an:


Ressource Belastung
Hauptspeicherzugriffe Prozessor
Dateioperationen Festplatte
Datenbankzugriff Server, Netzwerkverbindung

Tabelle 9.1 Parallelisierbare Ressourcen

Das Beispiel macht auch deutlich, dass die Nebenläufigkeit gut geplant werden muss. Nur wenn verzahnte Aktivitäten unterschiedliche Ressourcen verwenden, resultiert daraus auch auf Einprozessorsystemen ein Geschwindigkeitsvorteil. Daher ist ein paralleler Sortieralgorithmus nicht sinnvoll. Das zweite Problem ist die zusätzliche Synchronisation, die das Programmieren erschwert. Wir müssen auf das Ergebnis einer Operation warten, damit wir mit der Bearbeitung fortfahren können. Diesem Problem widmen wir uns in einem eigenen Abschnitt. Doch nun zur Programmierung von Threads in Java.






1 Mitunter sind die Begriffe »parallel« und »nebenläufig« nicht äquivalent definiert. Wir wollen sie aber in diesem Zusammenhang synonym benutzen.





Copyright (c) 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.


[Galileo Computing]

Galileo Press GmbH, Gartenstraße 24, 53229 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de