Die Programmiersprache Java stellt Threads als Sprachmittel zur Verfügung. Sie sind hier ein Konzept der Anwendungsprogrammierung und werden von der Virtuellen Java-Maschine (JVM) verwaltet. Java stellt mehrere Verfahren zur Einrichtung und Steuerung von Threads zur Verfügung, von denen hier nur das elementarste vorgestellt wird. An dieser Stelle ist ein Hinweis zum Programmieren in der Lehrveranstaltung angebracht: Die Lehrveranstaltung verteilte Systeme geht davon aus, dass die Studierenden zwei Semester Programmiergrundlagenausbildung in Java erfolgreich durchlaufen haben. Das heißt, dass in der Veranstaltung darauf aufbauend lediglich Konzepte des Programmierens verteilter Systeme vermittelt werden.
Wird ein Java-Programm gestartet, entsteht ein Prozess, der zunächst nur einen einzigen Thread enthält, der main-Thread genannt wird. Davon kann man sich an Hand des folgenden Programmbeispiels, das den Namen dieses Threads auf der Konsole ausgibt, leicht überzeugen:
Das Programm gibt bei einem Programmlauf
auf der Konsole aus. Der Prozess, der nach der Übersetzung des Programms MyThread.java in MyThread.class durch einen Konsolenbefehl wie
entsteht, beginnt mit einem Thread, der die Befehle der Methode main() sequenziell abarbeitet. Im Beispiel enthält main() nur einen einzigen Befehl, der in der println()-Methode auf die Klassenmethode currentThread() der Threadklasse zugreift, die eine Referenz auf das aktuelle Threadobjekt zurückliefert. Dessen Instanzmethode getName() wird dann benutzt, um zu dem Namen des aktuellen Threadobjekts zu gelangen.
Sollen von dem main-Thread oder einem anderen bereits vorhandenen Thread weitere Threads erzeugt werden, ist dafür entweder
In beiden Fällen ist eine run()-Methode auszuprogrammieren. Die Klasse Thread implementiert die Schnittstelle Runnable, die das Ausfüllen der Instanzmethode run() verlangt. Die Methode hat die Signatur:
Die Klasse Thread läßt run() leer und vererbt die Methode. Die run()-Methode legt die Threadaktivitäten fest.
Die Klasse Thread vererbt auch eine bereits ausprogrammierte Instanzmethode start(). Diese richtet den Thread ein und ruft dessen run()-Methode auf. Wird run() direkt aufgerufen, entsteht kein Thread, weil in diesem Fall nur ein elementarer Methodenaufruf vorgenommen wird. Im Folgenden werden die beiden Erzeugungsmethoden für Threads an einem sehr einfachen Beispiel kurz vorgestellt.
Bei dem ersten Verfahren wird die Klasse Thread abgeleitet. In dem folgenden Beispiel wird diese Ableitung in einer anderen Klasse instanziiert und die Instanz gestartet:
Die run()-Methode der Klasse MyThread legt die Threadaktivitäten fest. Im Beispiel führt der Thread die bereits bekannte Namensausgabe durch. Die Klasse Work enthält die main()-Methode. Wird Work.class gestartet, entsteht der main-Thread, der die Klasse MyThread instanziiert und den zugehörigen Thread einrichtet und startet. Danach führt der main-Thread ebenfalls eine Namensausgabe durch.
Die Ausgabe eines Programmlaufs von Work.class ist nicht genau vorhersagbar, weil sie von dem Verhalten des Schedulers der JVM abhängt. Je nachdem, welchem Thread zuerst Rechenzeit zugeteilt wird, wird auf der Konsole entweder
oder
ausgegeben.
Alternativ zur Ableitung der Threadklasse kann die Schnittstelle Runnable direkt implementiert werden. Dann allerdings muss für die Einrichtung des Threads ein geeigneter Konstruktor der Threadklasse verwendet werden. Das folgende Programm zeigt dieses Vorgehen, wobei wieder lediglich zwei Namensausgaben vorgenommen werden:
Bei diesem Vorgehen implementiert die Klasse MyThread die Schnittstelle Runnable direkt. Auch hier legt ihre run()-Methode die Threadaktivitäten fest. In der Klasse Work wird jetzt zunächst die Klasse MyThread instanziiert und danach eine Threadinstanz mit einem Konstruktor gebildet, dem eine Referenz auf die MyThreadinstanz übergeben wird. Auch bei diesem Verfahren wird durch die start()-Methode der Thread eingerichtet und gestartet. Das Ausgabeverhalten ist identisch zu dem des vorherigen Beispiels.
Jeder Thread hat einen Namen. Der main-Thread heißt zunächst main, alle weiteren heißen Thread-n mit einer laufenden Nummer n ab 0. Einem Thread, auch dem main-Thread, kann ein neuer Name gegeben werden. Dazu gibt es in der Threadklasse eine Instanzmethode namens setName(). Bei den Threads, außer dem main-Thread, kann zur Namensvergabe auch ein entsprechender Konstruktor verwendet werden.
Ein Java-Prozess mit allen seinen Threads kann durch den Aufruf von
beendet werden. Durch diesen Aufruf wird die ausführende JVM beendet. Die Variable n kann eine Fehlernummer transportieren, die von dem aufrufenden System abgefragt werden kann. Um diese Wertrückgabe etwas zu erläutern, betrachte man ein Javaprogramm, das mit dem Befehl
enden und unter Microsoft-Windows in einem Kommandoausführungsfenster gestartet werden soll. Dann kann dort nach dem Programmende zum Beispiel mit dem Konsolenkommando
auf diesen Rückgabewert zugegriffen werden. Wird das Javaprogramm in einer PowerShell gestartet, steht nach dem Programmende der Rückgabewert in der Shellvariablen $lastexitcode. Üblicherweise gibt ein Programm bei einem korrekten Ende den Wert Null zurück.
Enthält ein Prozess mehr als einen Thread, dann ist es möglich, dass mehrere dieser Threads ein und dieselbe Datenstruktur bearbeiten. In solchen Situationen kann das Beenden von einzelnen dieser Threads zu Inkonsistenzen, z.B. zu nur teilweise bearbeiteten Daten, führen.
Es ist empfehlenswert, einen einzelnen Thread nur dadurch zu beenden, indem seine run()-Methode fehlerfrei beendet wird. Dies kann durch einen explizit angegebenen return-Befehl oder durch eine Programmstruktur, wie die folgende, erreicht werden:
Soll dieser Thread an einer bestimmten Stelle der Threadaktivitäten beendet werden, kann die while-Schleife mit einem break-Befehl verlassen oder die Variable fertig auf true gesetzt und mit der continue-Anweisung ihre Prüfung am Schleifenanfang veranlasst werden.
Mit der Instanzmethode join() der Threadklasse wartet ein Thread, der eine Referenz auf eine Threadinstanz besitzt (das ist meistens der erzeugende Thread), auf das Ende des zu dieser Instanz gehörenden Threads. Die Methode hat die Signatur:
join() ist eine Instanzmethode. Es ist mit ihr nicht möglich, unspezifisch auf das Ende irgendeines Threads zu warten. Dass beim Programmieren mit Synchronisationsbefehlen wie join() sehr sorgfältig umgegangen werden muss, zeigt das folgende Beispiel, das zu einer Verklemmung (einem Deadlock) des main-Threads mit sich selbst führt:
Die Virtuelle Java-Maschine (JVM) enthält einen eigenen Scheduler, der die Zuweisung von Rechenzeit an die vorhandenen Threads vornimmt. Seine Strategie bestimmt, welchem Thread als nächstes der (oder ein) Prozessor zugeteilt wird. Er verwendet eine Prioritätssteuerung ohne Zeitscheibe mit 10 Prioritätsstufen. Stets erhält der Thread mit der jeweils höchsten Priorität Rechenzeit. Gibt es mehrere Threads mit der gleichen höchsten Priorität, wird von ihnen der Thread ausgewählt, der bisher am längsten gewartet hat. Man beachte, dass blockierte Threads dem Scheduling entzogen sind. Ihre Prioritität ist bedeutungslos.
Die 10 Prioritätsstufen sind durchnummeriert, wobei Threads auf der Stufe 1 die niedrigste und Threads auf der Stufe 10 die höchste Priorität haben. Dazu gibt es in der Threadklasse die drei Klassenvariablen:
Der main-Thread startet mit der Priorität 5 und kann sie ändern. Jeder neue Thread erbt die Priorität seines Erzeugers und kann diese für sich ebenfalls ändern. Die Instanzmethoden
der Threadklasse gestatten die Abfrage bzw. das Neusetzen der Priorität. Eine Beeinflussung der Normpriorität von 5 kann sinnvoll sein, wenn beispielsweise in einem Prozess den dialogintensiven Threads eine hohe und den rechenintensiven eine niedere Priorität zugewiesen wird. Allgemein ist zu bemerken, dass beim Programmieren kein Threadverhalten, wie z.B. eine Synchronisation, auf Prioritäten und damit auf eine Spekulation auf das Scheduling aufgebaut werden darf.
Ein Thread kann mit der folgenden Methode den Prozessor freigeben und damit den Scheduler der JVM implizit aufrufen:
Man beachte, dass durch einen yield()-Aufruf die Priorität des Threads nicht verändert wird. Hat er nach einem yield()-Aufruf immer noch die höchste Priorität und gibt es keinen laufbereiten Thread mit gleicher Priorität, bekommt er bei der nächsten Prozessorzeitzuweisung sofort wieder Rechenzeit.
Die Verwaltung von Threads benötigt Ressourcen und wirkt sich auf die Laufzeit des Prozesses aus. Es gibt viele Untersuchungen darüber, wie sich diese Laufzeit mit der Anzahl der Threads verändert. Üblicherweise zeigt sich durch die Nebenläufigkeit der Threads zunächst ein günstiges zeitliches Verhalten. Jedoch steigt nach dem Durchlaufen eines aufgaben- und prozessorabhängigen Minimums die Laufzeit wieder an.
Im Abschnitt 1.2.2 (Darstellung zeitlicher Abläufe) war der folgende Wartegraph gezeigt worden, der mit den begin-end- und parbegin-parend-Spachkonstruktionen nicht in ein Programm umgesetzt werden kann:
Mit Threads hingegen ist eine Überführung in ein Programm leicht möglich, allerdings auf Kosten der Programmstruktur. Das folgende Javaprogramm zeigt eine solche Umsetzung: