Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 7. Auflage |
<< | < | > | >> | API | Kapitel 23 - Multithreading |
Die Klasse Thread ist Bestandteil des Pakets java.lang und steht damit allen Anwendungen standardmäßig zur Verfügung. Thread stellt die Basismethoden zur Erzeugung, Kontrolle und zum Beenden von Threads zur Verfügung. Um einen konkreten Thread zu erzeugen, muss eine eigene Klasse aus Thread abgeleitet und die Methode run überlagert werden.
Mit Hilfe eines Aufrufs der Methode start wird der Thread gestartet und die weitere Ausführung an die Methode run übertragen. start wird nach dem Starten des Threads beendet und der Aufrufer kann parallel zum neu erzeugten Thread fortfahren.
Die Methode run sollte vom Programm niemals direkt aufgerufen werden. Um einen Thread zu starten, ist immer start aufzurufen. Dadurch wird der neue Thread erzeugt und initialisiert und ruft schließlich selbst run auf, um den Anwendungscode auszuführen. Ein direkter Aufruf von run würde dagegen keinen neuen Thread erzeugen, sondern wäre ein normaler Methodenaufruf wie jeder andere und würde direkt aus dem bereits laufenden Thread des Aufrufers erfolgen. |
|
Das folgende Beispiel zeigt einen einfachen Thread, der in einer Endlosschleife einen Zahlenwert hochzählt:
001 /* Listing2301.java */ 002 003 class MyThread2301 004 extends Thread 005 { 006 public void run() 007 { 008 int i = 0; 009 while (true) { 010 System.out.println(i++); 011 } 012 } 013 } 014 015 public class Listing2301 016 { 017 public static void main(String[] args) 018 { 019 MyThread2301 t = new MyThread2301(); 020 t.start(); 021 } 022 } |
Listing2301.java |
Zunächst wird hier ein neues Objekt vom Typ MyThread2301 instanziert. Die Ausführung eines Threads ist damit vorbereitet, aber noch nicht tatsächlich erfolgt. Erst durch den Aufruf von start wird ein neuer Thread erzeugt und durch einen impliziten Aufruf von run wird der Thread-Body gestartet. Da das Programm in einer Endlosschleife läuft, lässt es sich nur gewaltsam abbrechen (beispielsweise durch Drücken von [STRG]+[C]).
Im Gegensatz zu unseren bisherigen Beispielen wird dieses Programm nicht automatisch nach main beendet. Eine Java-Applikation wird nämlich immer nach Ende des letzten Threads beendet, der kein Hintergrund-Thread (Dämon) ist. Da ein einfaches Programm nur einen einzigen Vordergrund-Thread besitzt (nämlich den, in dem main läuft), wird es demnach beendet, wenn main beendet wird. Das Beispielprogramm erzeugt dagegen einen zusätzlichen Vordergrund-Thread und kann damit vom Interpreter erst dann beendet werden, wenn auch dieser Thread beendet wurde. Durch Aufruf von exit lassen sich auch Programme mit laufenden Vordergrund-Threads abbrechen. |
|
Zunächst einmal wird ein Thread dadurch beendet, dass das Ende seiner run-Methode erreicht ist. In manchen Fällen ist es jedoch erforderlich, den Thread von außen abzubrechen. Die bis zum JDK 1.1 übliche Vorgehensweise bestand darin, die Methode stop der Klasse Thread aufzurufen. Dadurch wurde der Thread abgebrochen und aus der Liste der aktiven Threads entfernt.
Wir wollen das vorige Beispiel erweitern und den Thread nach zwei Sekunden durch Aufruf von stop beenden:
001 /* Listing2302.java */ 002 003 class MyThread2302 004 extends Thread 005 { 006 public void run() 007 { 008 int i = 0; 009 while (true) { 010 System.out.println(i++); 011 } 012 } 013 } 014 015 public class Listing2302 016 { 017 public static void main(String[] args) 018 { 019 MyThread2302 t = new MyThread2302(); 020 t.start(); 021 try { 022 Thread.sleep(2000); 023 } catch (InterruptedException e) { 024 //nichts 025 } 026 t.stop(); 027 } 028 } |
Listing2302.java |
An diesem Beispiel kann man gut erkennen, dass der Thread tatsächlich parallel zum Hauptprogramm ausgeführt wird. Nach dem Aufruf von start beginnt einerseits die Zählschleife mit der Bildschirmausgabe, aber gleichzeitig fährt das Hauptprogramm mit dem Aufruf der sleep-Methode und dem Aufruf von stop fort. Beide Programmteile laufen also parallel ab.
Mit dem JDK 1.2 wurde die Methode stop als deprecated markiert, d.h., sie sollte nicht mehr verwendet werden. Der Grund dafür liegt in der potenziellen Unsicherheit des Aufrufs, denn es ist nicht voraussagbar und auch nicht definiert, an welcher Stelle ein Thread unterbrochen wird, wenn ein Aufruf von stop erfolgt. Es kann nämlich insbesondere vorkommen, dass der Abbruch innerhalb eines kritischen Abschnitts erfolgt (der mit dem synchronized-Schlüsselwort geschützt wurde) oder in einer anwendungsspezifischen Transaktion auftritt, die aus Konsistenzgründen nicht unterbrochen werden darf.
Die alternative Methode, einen Thread abzubrechen, besteht darin, im Thread selbst auf Unterbrechungsanforderungen zu reagieren. So könnte beispielsweise eine Membervariable cancelled eingeführt und beim Initialisieren des Threads auf false gesetzt werden. Mit Hilfe einer Methode cancel kann der Wert der Variable zu einem beliebigen Zeitpunkt auf true gesetzt werden. Aufgabe der Bearbeitungsroutine in run ist es nun, an geeigneten Stellen diese Variable abzufragen und für den Fall, dass sie true ist, die Methode run konsistent zu beenden.
Dabei darf cancelled natürlich nicht zu oft abgefragt werden, um das Programm nicht unnötig aufzublähen und das Laufzeitverhalten des Threads nicht zu sehr zu verschlechtern. Andererseits darf die Abfrage nicht zu selten erfolgen, damit es nicht zu lange dauert, bis auf eine Abbruchanforderung reagiert wird. Insbesondere darf es keine potenziellen Endlosschleifen geben, in denen cancelled überhaupt nicht abgefragt wird. Die Kunst besteht darin, diese gegensätzlichen Anforderungen sinnvoll zu vereinen.
Glücklicherweise gibt es in der Klasse Thread bereits einige Methoden, die einen solchen Mechanismus standardmäßig unterstützen:
public void interrupt() public boolean isInterrupted() public static boolean interrupted() |
java.lang.Thread |
Durch Aufruf von interrupt wird ein Flag gesetzt, das eine Unterbrechungsanforderung signalisiert. Durch Aufruf von isInterrupted kann der Thread feststellen, ob das Abbruchflag gesetzt wurde und der Thread beendet werden soll. Die statische Methode interrupted stellt den Status des Abbruchsflags beim aktuellen Thread fest. Ihr Aufruf entspricht dem Aufruf von currentThread().isInterrupted(), setzt aber zusätzlich das Abbruchflag auf seinen initialen Wert false zurück.
Wir wollen uns den Gebrauch dieser Methoden an einem Beispiel ansehen. Dazu soll ein Programm geschrieben werden, das in einem separaten Thread ununterbrochen Textzeilen auf dem Bildschirm ausgibt. Das Hauptprogramm soll den Thread erzeugen und nach 2 Sekunden durch einen Aufruf von interrupt eine Unterbrechungsanforderung erzeugen. Der Thread soll dann die aktuelle Zeile fertig ausgeben und anschließend terminieren.
001 /* Listing2303.java */ 002 003 public class Listing2303 004 extends Thread 005 { 006 int cnt = 0; 007 008 public void run() 009 { 010 while (true) { 011 if (isInterrupted()) { 012 break; 013 } 014 printLine(++cnt); 015 } 016 } 017 018 private void printLine(int cnt) 019 { 020 //Zeile ausgeben 021 System.out.print(cnt + ": "); 022 for (int i = 0; i < 30; ++i) { 023 System.out.print(i == cnt % 30 ? "* " : ". "); 024 } 025 System.out.println(); 026 //100 ms. warten 027 try { 028 Thread.sleep(100); 029 } catch (InterruptedException e) { 030 interrupt(); 031 } 032 } 033 034 public static void main(String[] args) 035 { 036 Listing2303 th = new Listing2303(); 037 { 038 //Thread starten 039 th.start(); 040 //2 Sekunden warten 041 try { 042 Thread.sleep(2000); 043 } catch (InterruptedException e) { 044 } 045 //Thread unterbrechen 046 th.interrupt(); 047 } 048 } 049 } |
Listing2303.java |
Die main-Methode ist leicht zu verstehen. Sie startet den Thread, wartet 2 Sekunden und ruft dann die Methode interrupt auf. In der Methode run wird in einer Endlosschleife durch Aufruf von printLine jeweils eine neue Zeile ausgegeben. Zuvor wird bei jedem Aufruf mit isInterrupted geprüft, ob das Abbruchflag gesetzt wurde. Ist das der Fall, wird keine weitere Zeile ausgegeben, sondern die Schleife (und mit ihr der Thread) beendet.
Innerhalb von printLine wird zunächst die Textzeile ausgegeben und dann eine Pause von 100 Millisekunden eingelegt. Da in der Methode keine Abfrage des Abbruchflags erfolgt, ist sichergestellt, dass die aktuelle Zeile selbst dann bis zum Ende ausgegeben wird, wenn der Aufruf von interrupt mitten in der Schleife zur Ausgabe der Bildschirmzeile erfolgt.
Da die Pause nach der Bildschirmausgabe mit 100 Millisekunden vermutlich länger dauert als die Bildschirmausgabe selbst, ist es recht wahrscheinlich, dass der Aufruf von interrupt während des Aufrufs von sleep erfolgt. Ist das der Fall, wird sleep mit einer InterruptedException abgebrochen (auch wenn die geforderte Zeitspanne noch nicht vollständig verstrichen ist). Wichtig ist hier, dass das Abbruchflag zurückgesetzt wird und der Aufruf von interrupt somit eigentlich verlorengehen würde, wenn er nicht direkt in der catch-Klausel behandelt würde. Wir rufen daher innerhalb der catch-Klausel interrupt erneut auf, um das Flag wieder zu setzen und run die Abbruchanforderung zu signalisieren. Alternativ hätten wir auch die Ausnahme an den Aufrufer weitergeben können und sie dort als Auslöser für das Ende der Ausgabeschleife betrachten können.
Die beiden anderen Methoden, die eine Ausnahme des Typs InterruptedException auslösen können, sind join der Klasse Thread und wait der Klasse Object. Auch sie setzen beim Auftreten der Ausnahme das Abbruchflag zurück und müssen daher in ähnlicher Weise behandelt werden. |
|
Sowohl innerhalb der Threads als auch innerhalb der Methode main wird ein Aufruf von Thread.sleep verwendet, um das Programm pausieren zu lassen. sleep ist eine statische Methode der Klasse Thread, die mit einem oder zwei Parametern aufgerufen werden kann:
public static void sleep(long millis) public static void sleep(long millis, int nanos) |
java.lang.Thread |
Die erste Version sorgt dafür, dass der aktuelle Prozess für die (in Millisekunden) angegebene Zeit unterbrochen wird. Die zweite erlaubt eine noch genauere Eingabe der Wartezeit, indem auch Bruchteile im Nanosekundenbereich angegeben werden können. In beiden Fällen wird die tatsächlich erzielbare Genauigkeit allerdings durch Hardwarerestriktionen der Zielmaschine begrenzt. Im Falle der aktuellen JDKs unter Windows liegt sie bei etwa 1 ms und ist damit wesentlich höher als currentTimeMillis. In Abschnitt 17.3.5 finden sich Beispielprogramme, mit denen die Auflösungen von currentTimeMillis und sleep ermittelt werden können.
Die Kapselung des Aufrufs von Thread.sleep innerhalb eines try-catch-Blocks ist erforderlich, weil sleep während der Wartezeit eine Ausnahme vom Typ InterruptedException auslösen kann. Ohne den try-catch-Block würde diese an den Aufrufer weitergegeben werden. |
|
Als Klassenmethode kann sleep aufgerufen werden, ohne dass eine Instanz der Klasse Thread verfügbar ist. Insbesondere kann die Methode auch dazu verwendet werden, das Hauptprogramm pausieren zu lassen, das ja nicht explizit als Thread erzeugt wurde. Dies funktioniert deshalb, weil beim Starten eines Java-Programms automatisch ein Thread für die Ausführung des Hauptprogramms angelegt wurde.
Mit dieser Methode kann festgestellt werden, ob ein Thread noch läuft.
public final boolean isAlive() |
java.lang.Thread |
isAlive gibt immer dann true zurück, wenn der Thread gestartet, aber noch nicht wieder beendet wurde. Beendet wird ein Thread, wenn das Ende der run-Methode erreicht ist oder wenn (in Prä-1.2-JDKs) die Methode stop aufgerufen wurde.
public final void join() throws InterruptedException |
java.lang.Thread |
Die Methode join wartet auf das Ende des Threads, für den sie aufgerufen wurde. Sie ermöglicht es damit, einen Prozess zu starten und (ähnlich einem Funktionsaufruf) mit der weiteren Ausführung so lange zu warten, bis der Prozess beendet ist. join gibt es auch mit einem long als Parameter. In diesem Fall wartet die Methode maximal die angegebene Zeit in Millisekunden und fährt nach Ablauf der Zeit auch dann fort, wenn der Prozess noch nicht beendet ist.
Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 7. Auflage, Addison Wesley, Version 7.0 |
<< | < | > | >> | API | © 1998, 2011 Guido Krüger & Heiko Hansen, http://www.javabuch.de |