Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger

Java ist auch eine Insel von Christian Ullenboom
Programmieren für die Java 2-Plattform in der Version 5 (Tiger-Release)
Buch: Java ist auch eine Insel
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 Die Zustände eines Threads
    gp 9.3.1 Das Ende eines Threads
    gp 9.3.2 Einen Thread höflich mit Interrupt beenden
    gp 9.3.3 Der stop() von außen
    gp 9.3.4 Das ThreadDeath-Objekt
    gp 9.3.5 Auf das Ende warten mit join()
    gp 9.3.6 Threads schlafen
    gp 9.3.7 Eine Zeituhr
  gp 9.4 Arbeit niederlegen und wieder aufnehmen
  gp 9.5 Priorität
    gp 9.5.1 Threads hoher Priorität und das AWT
    gp 9.5.2 Granularität und Vorrang
  gp 9.6 Dämonen
  gp 9.7 Kooperative und nichtkooperative Threads
  gp 9.8 Synchronisation über kritische Abschnitte
    gp 9.8.1 Gemeinsam genutzte Daten
    gp 9.8.2 Probleme beim gemeinsamen Zugriff und kritische Abschnitte
    gp 9.8.3 Punkte parallel initialisieren
    gp 9.8.4 i++ sieht atomar aus, ist es aber nicht
    gp 9.8.5 Abschnitte mit synchronized schützen
    gp 9.8.6 Monitore
    gp 9.8.7 Synchronized-Methode am Beispiel der Klasse StringBuffer
    gp 9.8.8 Synchronisierte Blöcke
    gp 9.8.9 Vor- und Nachteile von synchronisierten Blöcken und Methoden
    gp 9.8.10 Nachträglich synchronisieren
    gp 9.8.11 Monitore sind reentrant, gut für die Geschwindigkeit
    gp 9.8.12 Deadlocks
    gp 9.8.13 Erkennen von Deadlocks
  gp 9.9 Synchronisation über Warten und Benachrichtigen
    gp 9.9.1 Warten mit wait() und Aufwecken mit notify()
    gp 9.9.2 Falls der Lock fehlt: IllegalMonitorStateException
    gp 9.9.3 Mehrere Wartende und notifyAll()
    gp 9.9.4 wait() mit einer Zeitspanne
    gp 9.9.5 Beispiel Erzeuger-Verbraucher-Programm
    gp 9.9.6 Semaphoren
  gp 9.10 Atomares und frische Werte mit volatile
    gp 9.10.1 Das Paket java.util.concurrent.atomic
  gp 9.11 Aktive Threads in der Umgebung
  gp 9.12 Gruppen von Threads in einer Thread-Gruppe
    gp 9.12.1 Etwas über die aktuelle Thread-Gruppe herausfinden
    gp 9.12.2 Threads in einer Thread-Gruppe anlegen
    gp 9.12.3 Methoden von Thread und ThreadGroup im Vergleich
  gp 9.13 Die Klassen Timer und TimerTask
    gp 9.13.1 Job-Scheduler Quartz
  gp 9.14 Einen Abbruch der virtuellen Maschine erkennen


Galileo Computing

9.2 Threads erzeugedowntop

Für nebenläufige Programme in Java ist die Klasse Thread zuständig. Jeder Thread ist somit mit einem Exemplar dieser Klasse verbunden. Die von uns gewünschte, parallel auszuführende Aktivität programmieren wir in der Objektmethode run(). Wird der Thread gestartet, arbeitet es die run()-Programmzeilen parallel ab. Java bietet zur Programmierung von Threads zwei Möglichkeiten an, die wir nachfolgend vorstellen werden.


Galileo Computing

9.2.1 Threads über die Schnittstelle Runnable implementieredowntop

Eine Klasse, deren Exemplare Programmcode parallel ausführen sollen, muss die Schnittstelle Runnable implementieren. Sie schreibt eine run()-Methode vor.



interface java.lang.Runnable

gp  void run()
Diese Methode enthält den parallel auszuführenden Programmcode.

Abbildung
Hier klicken, um das Bild zu Vergrößern

Wir wollen zwei Threads angeben, wobei einer zwanzigmal das aktuelle Datum und die Uhrzeit ausgibt und der andere einfach eine Zahl.

Listing 9.1   FirstThread.java, Teil 1


class DateThread   implements Runnable  
{
    public void run()  
  {
    for ( int i = 0; i < 20; i++ )
      System.out.println( new Date() );
  }
}

class CounterThread   implements Runnable  
{
    public void run()  
  {
    for ( int i = 0; i < 20; i++ )
      System.out.println( i );
  }
}

In der Methode run() wird der auszuführende Programmcode eingebettet. Das heißt, bei dem einen Thread ist in einer Schleife das Date-Objekt auszugeben und in dem anderen der Schleifenzähler.


Galileo Computing

9.2.2 Threads über Runnable starten  downtop

Nun reicht es nicht aus, einfach die run()-Methode einer Klasse direkt aufzurufen. Würden wir dies tun, dann wäre nichts nebenläufig, sondern wir würden einfach eine Methode sequenziell ausführen. Damit der Programmcode nebenläufig zur Applikation läuft, müssen wir ein Thread-Objekt erzeugen und dann den Thread explizit starten. Dazu übergeben wir dem Konstruktor der Klasse Thread eine Referenz auf das Runnable-Objekt und rufen start() auf. Nachdem start() für den Thread eine Ablaufumgebung geschaffen hat, ruft es dann selbstständig die Methode run() genau einmal auf. Läuft der Thread schon, so wirft ein zweiter Aufruf der start()-Methode eine IllegalThreadStateException aus.



class java.lang.  Thread  
implements Runnable

gp  Thread( Runnable target )
Erzeugt einen neuen Thread mit einem Runnable, welches den parallel auszuführenden Programmcode vorgibt.
gp  void start()
Ein neuer Thread – neben dem die Methode aufrufenden Thread – wird gestartet. Der neue Thread führt die run()-Methode nebenläufig aus. Läuft der Thread schon, wird eine IllegalThreadStateException ausgelöst.

Listing 9.2   FirstThread.java, Teil 2


public class FirstThread
{
  public static void main( String args[] )
  {
    Thread t1 =   new Thread( new DateThread() );  
    t1.  start();  

    Thread t2 =   new Thread( new CounterThread() );  
    t2.  start();  
  }

}

Beim Starten des Programms erfolgt eine Ausgabe auf dem Bildschirm, die etwa so aussehen kann:


Thu Jan 18 15:40:20 CET 2001
0
1
2
3
4
5
6
7
8
9
Thu Jan 18 15:40:20 CET 2001
10
...

Deutlich ist die Verzahnung der beiden Prozesse zu erkennen. Was allerdings auf den ersten Blick etwas merkwürdig wirkt, ist die erste Zeile des Datum-Threads und viele weitere Zeilen des Zähl-Threads. Dies hat jedoch nichts zu sagen und zeigt deutlich den Nichtdeterminismus bei Threads. Interpretiert werden kann dies jedoch durch die unterschiedlichen Zeiten, die für die Aufgaben »gib Datum aus« und »gib Zahl« nötig sind.

Automatisches Starten während der Erzeugung

Erst der Aufruf der start()-Methode lässt den Thread ablaufen. Etwas eleganter ist der Weg, dass das Objekt seinen eigenen Thread verwaltet, der im Konstruktor gestartet wird. Dann muss dort ein Thread-Objekt für die Runnable-Umgebung angelegt und die start()-Methode ausgeführt werden. An einen Beispiel ausprobiert kann das so aussehen: Eine Klasse DateThreadAutoStart soll beim Anlegen automatisch einen Thread starten. Dazu initialisiert der Standard-Konstruktor von DateThreadAutoStart ein neues Thread-Objekt mit der this-Referenz als Argument.

Listing 9.3   DateThreadAutoStart.java


class DateThreadAutoStart implements Runnable
{
    DateThreadAutoStart()  
  {
      new Thread( this ).start();  
  }

  public void run()
  {
    for ( int i = 0; i < 20; i++ )
      System.out.println( new Date() );
  }
}

Hinweis   Wenn ein Thread im Konstruktor gebildet wird, dann sollte die Arbeitsweise bei Vererbung beachtet werden. Nehmen wir an, eine Klasse leitet von einer anderen Klasse ab, die im Konstruktor einen Thread startet. Bildet die Applikation ein Exemplar der Unterklasse, so werden in der Bildung des Objektes immer erst die Konstruktoren der Oberklasse aufgerufen. Das hat die Konsequenz, dass der Thread schon läuft, auch wenn das Objekt noch nicht ganz gebaut ist. Der Erzeugungsprozess ist erst dann abgeschlossen, wenn nach dem Aufruf der Konstruktoren der Oberklassen der eigene Konstruktor vollständig abgearbeitet wurde.


Galileo Computing

9.2.3 Die Klasse Thread erweitern  downtop

Da die Klasse Thread selbst die Schnittstelle Runnable implementiert und die run()-Methode mit leerem Programmcode bereitstellt, können wir auch Thread erweitern, wenn wir eigene parallele Aktivitäten programmieren wollen. Dann müssen wir kein Runnable-Exemplar mehr in den Konstruktor einfügen, denn wenn unsere Klasse Unterklasse von Thread ist, reicht ein Aufruf der geerbten Methode start(). Danach arbeitet das Programm direkt weiter, führt also kurze Zeit später die nächste Anweisung hinter start() aus. Dann lassen sich auch mehrere Exemplare der Thread-Klasse erzeugen.

Fassen wir die drei Schritte noch einmal zusammen:

gp  Wir bilden eine Unterklasse von Thread.
gp  Für ein Programmstück, das nebenläufig ablaufen soll, überschreiben wir die run()-Methode. run() darf keinen Parameter definieren, wenn doch, stimmt die Signatur nicht mit der in der Oberklasse Thread (beziehungsweise der Schnittstelle Runnable) überein und statt des Überschreibens gäbe es ein Überladen. Dann aber geschieht beim Starten des Threads nichts, weil die geerbte parameterlose (und leere) run()-Methode aufgerufen wird.
gp  Damit der Thread und damit das Programm startet, rufen wir die Objektmethode start() auf. Sie veranlasst die Laufzeitumgebung, Ressourcen zur Verfügung zu stellen. start() ruft dann automatisch run() auf. start() kann von uns auch überschrieben werden, was aber nur selten sinnvoll beziehungsweise nötig ist. Wir müssen dann darauf achten, super.start() aufzurufen, damit der Thread wirklich losrennt.

Beispiel   Ein paralleles Programm, das das Datum ausgibt:

class DateThreadExtends   extends Thread  
{
    public void run()  
  {
    for ( int i = 0; i < 20; i++ )
      System.out.println( new Date() );
  }
}

Abbildung
Hier klicken, um das Bild zu Vergrößern

Der Startschuss fällt für den Thread wieder beim Aufruf von start().


Thread t = new DateThreadExtends();
  t.start();  

Oder auch ohne Zwischenspeicherung der Objektreferenz:


new DateThreadExtends().start();

Damit wir als Thread-Benutzer nicht erst die start()-Methode aufrufen müssen, kann ein Thread sich auch wieder selbst starten. Der Konstruktor enthält einfach den Methodenaufruf start().


class DateThreadExtends extends Thread
{
  DateThreadExtends()
  {
      start();  
  }
  ...  der Rest bleibt ...
}


class java.lang.  Thread  
implements Runnable

gp  void run()
Diese Methode enthält den parallel auszuführenden Programmcode.

Was passiert, wenn wir statt start() nun run() aufrufen?

Ein Programmierfehler, der hin und wieder passiert, ist Folgender: Anstatt start() rufen wir aus Versehen run() auf. Was geschieht? Fast genau das Gleiche wie bei start(), nur mit dem Unterschied, dass die Objektmethode run() nicht parallel zum übrigen Programm abgearbeitet wird. Der aktuelle Thread bearbeitet die run()-Methode sequenziell, bis sie zu Ende ist und die Anweisungen nach dem Aufruf an die Reihe kommen. Der Fehler fällt nicht immer direkt auf, denn die Aktionen in run() finden ja statt – nur nicht nebenläufig.

Runnable ist kein Thread. Wie können wir die Thread-Methoden nutzen?

Eine Erweiterung der Klasse Thread hat den Vorteil, dass alle geerbten Methoden sofort genutzt werden können. Wenn wir Runnable implementieren, dann genießen wir den Vorteil nicht. Wie lassen sich dann trotzdem die Thread-Methoden nutzen? Dazu bietet Thread die Klassenmethode currentThread() an, um die Objektreferenz für das Thread-Exemplar zu holen, das gerade diese Methode ausführt.


Beispiel   Die aktuelle Priorität des laufenden Threads soll ausgegeben werden.

System.out.println( Thread.currentThread().getPriority() );


Galileo Computing

9.2.4 Erweitern von Thread oder Implementieren von Runnable?  toptop

Eine unangenehme Begleiterscheinung des Erweiterns von Thread ist, dass die Möglichkeit der Vererbung nicht genutzt werden kann, wenn die neue Thread-Unterklasse noch weitere Funktionalität erben soll – Mehrfachvererbung ist in Java nicht möglich. Dies ist häufig das Dilemma, wenn ein Applet nebenläufige Programmstücke besitzt. Hier können allerdings innere Klassen das Problem lösen. Die Tabelle fasst die Vor- und Nachteile der beiden Alternativen noch einmal zusammen:


Tabelle 9.2   Vor- und Nachteile von extends Thread und implements Runnable

  Vorteil Nachteil
Ableiten von Thread (extends Thread) Programmcode in run() kann die Methoden der Klasse Thread nutzen. Da es in Java keine Mehrfachvererbung gibt, kann die Klasse nur Thread erweitern.
Implementieren von Runnable (implements
Runnable)
Die Klasse kann von einer anderen, problemspezifischen Klasse erben. Kann sich nur mit Umwegen selbst starten; allgemein: Thread-Methoden können nur über Umwege genutzt werden.





Copyright © 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