Threads


... [ Seminar WWW und JAVA] ... [ Applets und das Java AWT ] ... [ Literaturliste ] ...


Übersicht: Threads


Einleitung

Java ist eine multithreading-fähige Programmiersprache, d.h. sie unterstützt direkt die Programmierung von parallelablaufenden Programmsegmenten. Ein Thread ist ein eigener Prozeß, der ein solches Programmsegment abarbeitet. Durch parallellaufende Programmteile kann die Performance des Applets bzw. der Applikation gesteigert werden. Bei einem Mehrprozessorsystem können Threads vom Java-Laufzeitsystem mit Hilfe des Betriebssystems auf unterschiedliche Prozessoren verteilt werden. Bei einem Computer mit nur einem Prozessor ist keine echte Parallelarbeit möglich. Das Java-Laufzeitsystem kann aber durch Umschalten zwischen den Prozessen den Eindruck von Parallelität erzeugen.

Mit Hilfe eines Threads kann beispielsweise auch ein kleiner "Film" innerhalb eines Applets laufen. Während der Wartezeiten zwischen dem Anzeigen der Einzelbilder geht der Thread "schlafen", in dieser Zeit kann das Applet (bzw. der Prozessor) andere Ereignisse bearbeiten.


Quellcode

Jeder Thread hat automatisch eine mittlere Priorität, die im Programm geändert werden kann. Bei dem Wettbewerb der Prozesse um die Verarbeitung auf einem oder mehreren Prozessoren (systemabängig) werden Threads mit höherer Priorität denen mit niedrigerer vorgezogen. Eine hohe Priorität ist aber noch keine Garantie dafür, daß der Thread laufen kann.

Das java.lang Paket stellt eine Thread-Klasse zur Verfügung, die Methoden zum Starten, Ausführen und Beenden von Threads enthält und den Status des Threads überwacht. Zusätzlich zu dieser Klasse gibt es noch die ThreadGroup-Klasse, in der mehrere Threads zusammengefaßt und gemeinsam gesteuert werden können. Im allgemeinen werden Thread-Gruppen nur von Applikationen auf Systemebene benutzt.

Für die Synchronisation unterhält die Thread-Unterstützung einige einfache Befehle. Mit Hilfe der synchronized-Anweisung können "kritische Teile", die nicht gleichzeitig ablaufen dürfen, geschützt werden.


Sperren von kritischen Abschnitten

Kritische Abschnitte sind Programmteile, die nicht parallel ablaufen bzw. unterbrochen werden dürfen, da es andernfalls zu Inkonsistenzen kommen kann. Java stellt einen Monitormechanismus zur Verfügung, der einen Ausschnitt eines Programms überwacht und dafür sorgt, daß er nicht von mehreren Threads gleichzeitig benutzt wird.

Das folgende Beispiel zeigt einen kritischen Abschnitt, der nicht von zwei Threads gleichzeitig durchlaufen werden darf, da er z.B. zwischen dem Bearbeiten der Variablen und der Returnanweisung "unterbrechbar" ist.

=> Der Wert von zahl ist am Ende 1, obwohl zahl von beiden Threads erhöht wurde.

Um den kritischen Abschnitt zu schützen, muß die Syntax synchronized (Ausdruck) Anweisung verwendet werden, d.h. für das Beispiel:

=> Der Wert von zahl ist nun zwei.

Für jedes Objekt legt Java zur Laufzeit einen Monitor an. Dieser Monitor (oder "Lock") sperrt das Objekt für den Zugriff von Threads, wenn ein Thread eine synchronized-Instanzmethode aus diesem Objekt benutzen will. Dieser Thread erhält den alleinigen Zugang zu dem Objekt. Wenn er die Instanzmethode beendet hat, wird die Sperre wieder aufgehoben.

Das "Locken" und "Unlocken" wird automatisch vom System geregelt. So wird sichergestellt, daß jeweils nur ein Thread den Zugang zum kritischen Abschnitt erhält bzw., daß er so lange blockiert wird, bis ein anderer Thread, der diesen Abschnitt gerade bearbeitet, den kritischen Bereich wieder freigegeben hat. Gleiches gilt entsprechend auch für eine Klasse bei einem Aufruf einer synchronized-Klassenmethode.


Inkonsistenzen trotz Synchronisation

Werden mehrere Threads im Programm verwendet und keine weiteren Angaben über die Reihenfolge der Bearbeitung angegeben, bleibt es dem System überlassen, welcher Thread zuerst Rechenzeit erhält.

Benutzen mehrere Threads das gleiche Objekt, kann es trotz Synchronisation zu Inkonsistenten kommen.

Beispiel:

Angenommen, es gibt zwei Threads, der eine ruft die Methode neu() auf, der andere die Methode drucke(), dann kann das System einen der folgenden Abläufe wählen:

Der 1. Thread ruft neu() auf und blockiert damit A. => a = 3, b = 4 => A wird wieder freigegeben.

Der 2. Thread ruft drucke() auf. => Ausgabe: "a = 3, b = 4"

Der 1. Thread ruft drucke() auf. => Ausgabe: "a = 1, b = 2"

Der 2. Thread ruft neu() auf und blockiert damit A. => a = 3, b = 4 => A wird wieder freigegeben.

Der 1. Thread ruft drucke() auf. => Ausgabe: "a = 1,

Der 2. Thread ruft neu() auf und blockiert damit A. => a = 3, b = 4 => A wird wieder freigegeben.

Der 1. Thread arbeitet weiter: b = 4".

Selbst, wenn beide Methoden den synchronized-Zusatz erhalten, kann sich das System immer noch zwischen einem der beiden oberen Ausführungen entscheiden. Der Unterschied liegt darin, daß jetzt das Objekt A bei beiden Aufrufen für weitere Zugriffe gesperrt wird, und damit die letzte Möglichkeit entfällt.


Erzeugen von Threads

Java stellt zwei Wege zur Verfügung, Threads zu erzeugen:

Nachdem die run()-Methode implementiert wurde, kann eine Instanz auf die neue Klasse angelegt werden. Um einen Thread zu erzeugen, wird nun ein Thread-Objekt instanziert, wobei die Instanz der neuen Klasse als Parameter an den Konstruktor übergeben wird.

In beiden Fällen muß die neue Klasse mindestens die run()-Methode definieren, da diese die Anweisungsfolge bestimmt, die parallel zu anderen Threads abgearbeitet werden soll. Die run()-Methode wird durch die start()-Methode des Thread-Objektes aufgerufen.

Wie der Grafik "java.lang Paket" zu entnehmen ist, ist die Thread-Klasse selbst eine Implementierung der Schablone Runnable.


Zustandsmenge von Threads

Ein Thread befindet sich immer in einem der vier Zustände:

Im Zustand Neu befindet sich der Thread, nachdem er durch den Konstruktoraufruf angelegt wurde. In diesem Zustand darf der Thread nur seine Methoden start() und stop() aufrufen.

In den Zustand Bereit gelangt der Thread, wenn start() aufgerufen wird. Diese Methode ruft automatisch die run()-Methode auf, die das Programmsegment, das der Threads durchlaufen soll, enthält. Ob der Thread ablaufen kann, hängt von seiner Priorität und der Anzahl anderer Threads ab, die sich ebenfalls in der Bereitmenge befinden.

Ist der Thread im Zustand Blockiert, ist seine Abarbeitung unterbrochen worden. Die Tabelle (s.u.) zeigt Methoden, durch die diese Unterbrechungen hervorgerufen und wieder aufgehoben werden können:

Den Zustand Fertig erreicht ein Thread, wenn seine run()-Methode abgearbeitet werden konnte, oder er durch den expliziten Aufruf seiner stop()-Methode beendet wurde.

      Bereit => Blockiert

      Blockiert => Bereit

      yield()

      automatisch

      tritt verbleibende Prozessorzeit ab

      sleep()

      automatisch

      wartet eine bestimmte Zeit ab

      join()

      automatisch

      wartet auf das Ende eines anderen Threads

      suspend()

      resume()

      Unterbrechung

      wait()

      notify()/notifyAll()

      wartet auf eine Ressource

      (einen anderen Thread)

yield() veranlaßt den Thread, seine verbleibende Prozessorzeit einem anderen Thread mit einer höheren oder gleichen Priorität zu überlassen. Sind solche Threads nicht vorhanden oder blockiert, erhält er wieder den Zugang zum Prozessor. Prioritäten können zwischen eins (MIN_PRIORITY) über fünf (NORM_PRIORITY) bis zehn (MAX_PRIORITY) gestaffelt werden. Das Java-Laufzeitsystem vergibt vorhandene Rechenzeit zuerst an Threads mit maximaler Priorität. Sind davon mehrere vorhanden, wird irgendeiner von ihnen als zuerst bearbeitet. Erst, wenn alle Threads mit der hohen Priorität beendet oder blockiert sind, werden Threads mit niedrigerer Priorität bearbeitet.

sleep() veranlaßt den Thread für eine angegebene Zeit nicht am Wettbewerb um einen Prozessor teilzunehmen. Diese Methode benutzt dabei yield().

join() wird benötigt, um den Kontrollfluß mehrerer Threads wieder zusammenzuführen. Wird ein Thread innerhalb eines anderen Threads erzeugt, so wird der neue Thread als "Kind" und der alte als "Vater" bezeichnet. Soll z.B. der Vaterprozeß beendet werden, kann er durch join() angehalten werden, bis auch der Kindprozeß beendet ist und beide Prozesse zusammengeführt werden. Auf Intel-Plattformen kann das Beenden ohne die Wiedervereinigung zum Programmabbruch führen, auf SUN-Plattformen hingegen kann hierbei auf join() verzichtet werden.

suspend() hält einen Thread an, bis ihm durch resume() mitgeteilt wird, daß er wieder am Wettbewerb um die Betriebsmittel teilnehmen kann. Ein Thread kann sich selbst anhalten oder von einem anderen Thread angehalten werden. Wiedererweckt werden kann er nur von einem anderen Thread.

wait(), notify() und notifyAll() sind Instanzmethoden von Objekt und werden zur Synchronisation benutzt. Versucht ein Thread, ein gesperrtes Objekt zu benutzen, wird er durch wait() veranlaßt, so lange zu warten, bis ihm durch notify() bzw. notifyAll() mitgeteilt wird, daß das Objekt für ihn freigegeben wurde.


Beispiel: Uhr



Quellcode

Die Links in diesem Beispiel sind Verweise auf den jeweiligen Zustand des Threads bzw. sollen den Programmfluß simulieren.

Applet laden / beenden


Daemons

Daemons sind Threads, die im allgemeinen eine Endlosschleife enthalten, in der sie für andere Threads Dienste erbringen.

Durch die Methode setDaemon(true) wird ein "normaler" Thread als Daemon gekennzeichnet, das hat den Vorteil, daß dieser Thread automatisch beendet werden kann, wenn seine Dienste nicht mehr von anderen Threads benötigt werden. Sind im Laufzeitsystem nur noch Daemon-Threads vorhanden, kann das Programm beendet werden.


... [ Seminar WWW und JAVA] ... [ Applets und das Java AWT] ... [ Threads ] ... [ Literaturliste ] ...