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.3 Die Zustände eines Threaddowntop

Bei einem Thread-Exemplar können wir einige Zustände feststellen: noch nicht erzeugt, laufend (vom Scheduler berücksichtigt), nicht laufend (vom Scheduler nicht berücksichtigt), wartend und beendet. Wie das Leben eines Thread-Objekts beginnt, haben wir schon gelernt – es muss nur mit new erzeugt werden, ist aber noch nicht im Zustand ausführend. Durch start() gelangt der Thread in den Zustand »ausführbar« beziehungsweise »laufend«. Der Zustand kann sich ändern, wenn ein anderer Thread in einem Einprozessorsystem zur Ausführung gelangt und dann dem aktuellen Thread den Prozessor entzieht. Anschließend geht der vorherige Thread für einen kurzen Moment in den wartenden Zustand. Dieser wird auch erreicht, wenn wir mittels spezieller Synchronisationstechniken in einem Wartezustand verweilen. Nachdem die Aktivität des Thread-Objekts beendet wurde, kann es nicht mehr aktiviert werden und es ist tot, also beendet.


Galileo Computing

9.3.1 Das Ende eines Threads  downtop

Es gibt Threads, die dauernd laufen, weil sie zum Beispiel Server-Funktionen implementieren. Andere Threads führen einmalig eine Operation aus und sind danach beendet. Allgemein ist ein Thread beendet, wenn eine der folgenden Bedingungen zutrifft:

gp  Die run()-Methode wurde ohne Fehler beendet. Wenn wir eine Endlosschleife programmieren, würde diese dann potenziell einen nie endenden Thread bilden.
gp  In der run()-Methode tritt eine Exception auf, die die Methode beendet.
gp  Der Thread wurde von außen abgebrochen. Dazu dient die prinzipbedingt problematische Methode stop(), von deren Verwendung abgeraten wird und die auch veraltet ist.
gp  Die virtuelle Maschine wird beendet, die alle Threads mit ins Grab nimmt.

Wenn der Thread einen Fehler melden soll

Da ein Thread nebenläufig arbeitet, kann die run()-Funktion synchron keine Exception melden. Wer sollte auch an welcher Stelle darauf hören? Eine Lösung für das Problem ist ein Listener, der sich beim Thread anmeldet und informiert wird, ob der Thread seine Arbeit machen konnte oder nicht. Eine andere Lösung ist, eine Objektvariable erfolg einzuführen, die von außen abgefragt werden kann. Der Thread kann dann diese Erfolg-Variable setzen. Soll eine Exception gemeldet werden, kann dieses Exception-Objekt über den Listener transportiert werden oder einfach die Erfolg-Variable dieses Exception-Objekts referenzieren.


Galileo Computing

9.3.2 Einen Thread höflich mit Interrupt beendedowntop

Der Thread ist in der Regel zu Ende, wenn die run()-Methode ordentlich bis zum Ende ausgeführt wurde. Enthält eine run()-Methode jedoch eine Endlosschleife – wie etwa bei einem Server, der auf eingehende Anfragen wartet –, so muss der Thread von außen zur Kapitulation gezwungen werden. Die nahe liegende Möglichkeit, mit der Thread-Methode stop() einen Thread abzuwürgen, wollen wir an anderer Stelle diskutieren.

Wenn wir schon nicht von außen den Thread beenden wollen, dann können wir ihn bitten, dass er seine Arbeit aufgibt. Periodisch müsste er dann nur überprüfen, ob von außen jemand den Abbruchswunsch geäußert hat. Dies könnte etwa durch eine Objektvariable stopp geschehen, die beim Initialisieren den Startwert false bekäme. Periodisch würden wir nun in run() testen, ob stopp uns zur Aufgabe zwingt oder wir weitermachen können. Von außen kann der Thread durch die einfache Zuweisung stopp=true angehalten werden. Diese Lösung ist einfach und effektiv. Sie funktioniert jedoch nur dann gut, wenn stopp regelmäßig abgefragt wird. Leider verbraucht die dauernde Fragerei auch Laufzeit.

Die Methoden interrupt() und isInterrupted()

Natürlich haben die Java-Entwickler schon an eine Lösung gedacht und drei Methoden implementiert, mit denen sich eine Unterbrechung ankündigen und abfragen lässt. Mit der Methode interrupt() wird in einem Thread-Objekt von außen ein internes Flag gesetzt, welches dann in der run()-Methode durch isInterrupted() periodisch abgefragt werden kann.

Das nachfolgende Programm soll alle halbe Sekunde eine Meldung auf dem Bildschirm ausgeben. Nach zwei Sekunden wird der Unterbrechungs-Wunsch mit interrupt() gemeldet. Auf dieses Signal achtet die sonst unendlich laufende Schleife und bricht ab.

Listing 9.4   ThreadusInterruptus.java


class ThreadusInterruptus extends Thread
{
  public void run()
  {
    System.out.println( "Der Anfang" );

    while ( ! isInterrupted() )
    {
      System.out.println( "Und er läuft und er läuft und er läuft" );

      try
      {
        Thread.sleep( 500 );
      }
      catch ( InterruptedException e )
      {
      interrupt();
      System.out.println( "Unterbrechung in sleep()" );
    }
  }

  System.out.println( "Das Ende" );
}

public static void main( String args[] )
{
  ThreadusInterruptus t = new ThreadusInterruptus();

  t.start();

  try {
    Thread.sleep( 2000 );
    } catch ( InterruptedException e ) { }

    t.interrupt();
  }
}

Die Ausgabe zeigt hübsch die Ablaufsequenz:


Der Anfang
Und er läuft und er läuft und er läuft
Und er läuft und er läuft und er läuft
Und er läuft und er läuft und er läuft
Und er läuft und er läuft und er läuft
Unterbrechung in sleep()
Das Ende

Die run()-Methode im Thread ist so implementiert, dass die Schleife genau dann verlassen wird, wenn isInterrupted() den Wert true ergibt, also von außen die interrupt()-Methode für dieses Thread-Exemplar aufgerufen wurde. Genau dies geschieht in der main()-Methode. Auf den ersten Blick ist das Programm leicht verständlich, doch vermutlich erzeugt das interrupt() im catch-Block die Aufmerksamkeit. Stünde diese Zeile dort nicht, würde das Programm aller Wahrscheinlichkeit nach nicht funktionieren. Das Geheimnis ist Folgendes: Wenn die Ausgabe nur alle halbe Sekunde stattfindet, dann befindet sich der Thread fast die gesamte Zeit in der Schlaf-Methode sleep(). Also wird vermutlich der interrupt() den Thread gerade beim Schlafen stören. Genau dann wird sleep() durch InterruptedException unterbrochen, und der catch()-Behandler fängt die Ausnahme ein. Jetzt passiert aber etwas Unerwartetes: Durch die Unterbrechung wird das interne Flag zurückgesetzt, so dass isInterrupted() meint, die Unterbrechung habe gar nicht stattgefunden. Daher muss interrupt() erneut aufgerufen werden, da das Abbruch-Flag neu gesetzt werden muss und isInterrupted() das Ende bestimmen kann.

Wenn wir mit der isInterrupted()-Methode arbeiten, dann müssen wir beachten, dass auch die Methoden join() und wait() durch die InterruptedException das Flag löschen.


Hinweis   Die Methoden sleep(), wait() und join() lösen alle eine InterruptedException aus, wenn sie durch die Methode interrupt() unterbrochen werden. Das heißt, interrupt() beendet diese Methoden mit der Ausnahme.

Zusammenfassung: interrupted(), isInterrupted() und interrupt()

Die Methodennamen sind verwirrend gewählt, so dass wir die Aufgaben noch einmal zusammenfassen wollen. Die Objektmethode interrupt() setzt in einem (anderen) Thread-Objekt ein Flag, dass es einen Antrag gab, den Thread zu beenden – es beendet nicht den Thread, obwohl der Methodenname das vermuten lässt. Dieses Flag lässt sich mit der Objektmethode isInterrupted() abfragen. In der Regel wird dies innerhalb einer Schleife geschehen, die darüber bestimmt, ob die Aktivität des Threads fortgesetzt werden soll. Die Klassenmethode interrupted() ist dagegen mehr als nur eine Anfragemethode (daher fehlt auch der Präfix is-). Sie testet zwar das entsprechende Flag des aktuell laufenden Threads, wie currentThread().isInterrupted(), aber modifiziert ihn auch, so dass er danach gelöscht ist. Zwei aufeinander folgende Aufrufe von interrupted() führen daher zu einem false, es sei denn, in der Zwischenzeit kam eine weitere Unterbrechung.


Galileo Computing

9.3.3 Der stop() von außen  downtop

Wenn ein Thread nicht auf interrupt() hört, dann muss zum Abbruch die veraltete Methode stop() eingesetzt werden.

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

Dass die Methode stop() veraltet ist, zeigt eine unterschlängelte Linie und ein Symbol am linken Rand an. Steht der Cursor auf der problematischen Zeile zeigt eine Fehlermeldung ebenfalls das Problem.

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

Doch deprecated gibt uns schon einen guten Hinweis, dass wir dies besser nicht machen sollten. (Leider gibt es hier, im Gegensatz zu den meisten anderen veralteten Methoden, keinen einfachen, empfohlenen Ersatz.) Überschreiben können wir stop() auch nicht, da es final ist. Wenn wir einen Thread von außen beenden, geben wir ihm keine Chance mehr, seinen Zustand konsistent zu verlassen. Zudem kann die Unterbrechung an beliebiger Stelle erfolgen, so dass angeforderte Ressourcen frei in der Luft hängen können.



class java.lang.  Thread  
implements Runnable

gp  void stop()
Wurde der Thread gar nicht gestartet, kehrt die Funktion sofort zurück. Andernfalls wird über checkAccess() geprüft, ob wir überhaupt das Recht haben, den Thread abzuwürgen. Dann wird der Thread beendet, egal was er gerade gemacht hat, und das Letzte, was der Thread jetzt noch kann, ist sein Testament in Form eines ThreadDeath-Objekts als Exception anzuzeigen.
gp  void checkAccess()
Findet heraus, ob wir die Möglichkeit haben, den Zustand oder die Eigenschaften des Thread-Objekts von außen zu ändern. checkAccess() der Klasse Thread ruft die check Access()-Methode vom Security-Manager auf; möglicherweise bekommen wir eine SecurityException.

Galileo Computing

9.3.4 Das ThreadDeath-Objekt  downtop

So unmöglich ist das Reagieren auf ein stop() auch nicht. Immer dann, wenn ein Thread mit stop() zu Ende kommen soll, löst die JVM eine ThreadDeath-Ausnahme aus, die letztendlich den Thread beendet. ThreadDeath ist eine Unterklasse von Error. Error ist wiederum von Throwable abgeleitet, so dass ThreadDeath mit einem try/catch-Block abgefangen werden kann. Die Java-Entwickler haben ThreadDeath nicht zu einer Unterklasse von Exception gemacht, weil sie nicht wollten, dass ThreadDeath bei einer allgemeinen Exception-Behandlung über catch(Exception e) abgefangen wird.

Wenn wir ThreadDeath auffangen, dann können wir noch auf den Tod reagieren und Aufräumarbeiten erlauben. Wir sollten aber nicht vergessen, anschließend das aufgefangene ThreadDeath-Objekt wieder auszulösen, denn sonst wird der Thread nicht beendet.

Listing 9.5   ThreadWiederbelebung.java


class ThreadWiederbelebung
{
  public static void main( String args[] )
  {
    class RunEndlos implements Runnable
    {
      public void run()
      {
        try
        {
          while ( true )
            ;
        }
          catch ( ThreadDeath td )  
        {
          System.out.println( "Da haben wir aber noch mal Glück gehabt" );
          throw td;
        }
      }
    }

    Thread t = new Thread( new RunEndlos() );
    t.start();
    t.stop();
  }
}

ThreadDeath kann auch verwendet werden, um das aktuell laufende Programm zu beenden (aber System.exit() ist weniger extravagant).


throw new ThreadDeath();

Galileo Computing

9.3.5 Auf das Ende warten mit join()  downtop

Ein Thread kann keine Ergebnisse wie eine Funktion nach außen geben, da die run()-Methode den Ergebnistyp void hat. Da ein nebenläufiger Thread zudem asynchron arbeitet, wissen wir noch nicht einmal, wann wir das Ergebnis erwarten können. Die Übertragung von Werten ist jedoch kein Problem. Hier können Klassenvariablen und auch Objektvariablen helfen, denn über sie können wir kommunizieren. Jetzt fehlt nur noch, dass wir auf das Ende der Aktivität eines Threads warten können. Das geht mit der Methode join().


Beispiel   Ein Thread T legt in der Variablen result ein Ergebnis ab. Wir können die Auswirkungen von join() sehen, wenn wir die auskommentierte Zeile hineinnehmen.

Listing 9.6   JoinTheThread.java


class JoinTheThread
{
  static class JoinerThread extends Thread
  {
    public int result;

    public void run()
    {
      result = 1;
    }
  }

  public static void main( String args[] ) throws Exception
  {
    JoinerThread t = new JoinerThread();
    t.start();

  //    t.join();  

    System.out.println( t.result );
  }
}

Ohne den Aufruf von join() wird als Ergebnis 0 ausgegeben, denn das Starten des Threads kostet etwas Zeit. In dieser Zeit aber geben wir die nicht initialisierte Klassenvariable aus. Nehmen wir join() hinein, wird die run()-Methode zu Ende ausgeführt und der Thread setzt die Variable result auf 1. Das sehen wir dann auf dem Bildschirm.



class java.lang.  Thread  
implements Runnable

gp  final void join() throws InterruptedException
Der aktuell ausgeführte Thread wartet auf den Thread, für den die Methode aufgerufen wird, bis dieser beendet ist.
gp  final synchronized void join( long millis ) throws InterruptedException
Wie join(), doch wartet diese Variante höchstens millis Millisekunden. Wurde der Thread bis dahin nicht vollständig beendet, fährt das Programm fort. Auf diese Weise kann versucht werden, innerhalb einer bestimmten Zeitspanne auf den Thread zu warten, sonst aber weiterzumachen. Ist millis gleich 0, so hat dies die gleiche Wirkung wie join().
gp  final synchronized void join ( long millis, int nanos )
  throws InterruptedException
Wie join(long), jedoch mit potenziell genauerer Angabe der maximalen Wartezeit

Warten auf den Langsamsten

Große Probleme können in mehrere Teile zerlegt werden, und jedes Teilproblem kann dann von einem Thread gelöst werden. Das ist insbesondere bei Mehrprozessorsystemen eine lohnenswerte Investition. Zum Schluss müssen wir nur noch darauf warten, dass die Threads zum Ende gekommen sind und das Ergebnis einsammeln. Dazu eignet sich join() gut.


Beispiel   Zwei Threads A und B arbeiten an einem Problem. Eine Methode go() erzeugt die Threads und wartet, bis beide ihre Aufgabe erledigt haben. Dann könnte etwa ein anderer Thread die von A und B benutzten Ressourcen wieder nutzen.

void go() throws Exception
{
  Thread a = new A();
  Thread b = new B();
  a.start();
  b.start();
  a.  join()  ;
  b.  join()  ;
}

Es ist unerheblich, wessen join() wir zuerst aufrufen, da wir sowieso auf den langsamsten Thread warten müssen. Wenn ein Thread schon beendet ist, dann kehrt join() sofort zurück.

Eine andere Lösung für zusammenlaufende Threads ist, diese in einer Thread-Gruppe zusammenzufassen. Dann können sie zusammen behandelt werden, so dass nur das Ende der Thread-Gruppe beobachtet wird.


Galileo Computing

9.3.6 Threads schlafen  downtop

Manchmal ist es notwendig, einen Thread für eine bestimmte Zeit anzuhalten. Dazu dient die überladende Klassenfunktion sleep(). Etwas erstaunlich ist sicherlich, dass das keine Objektfunktion ist, die jedes Thread-Objekt anbietet, sondern eine statische Funktion. Ein Grund wird sein, dass dadurch verhindert wird, externe Threads zu beeinflussen. Es ist nicht möglich, einen fremden Thread, dessen Referenz wir haben, einfach ein paar Sekunden schlafen zu legen.



class java.lang.  Thread  
implements Runnable

gp  static void sleep( long millis ) throws InterruptedException
Der aktuell ausgeführte Thread wird mindestens millis Millisekunden eingeschläfert. Unterbricht ein anderer Thread den schlafenden, so wird vorzeitig eine Interrupted Exception ausgelöst.
gp  static void sleep( long millis, int nanos ) throws InterruptedException
Der aktuell ausgeführte Thread wird mindestens millis Millisekunden und zusätzlich nanos Nanosekunden eingeschläfert. Im Gegensatz zu sleep(long) wird bei einer negativen Millisekundenanzahl eine IllegalArgumentException ausgelöst, ebenso wird diese Exception ausgelöst, wenn die Nanosekundenanzahl nicht zwischen 0 und 999999 liegt.

Beispiel   Die Applikation soll zwei Sekunden lang schlafen.

try {
    Thread.sleep  ( 2000 );
} catch ( InterruptedException e ) { }

Die Unterbrechung sitzt in einem zwingenden try/catch-Block, da eine Exception ausgelöst wird, wenn der Thread unterbrochen wird.

Praktisch wird das Erweitern der Klasse Thread bei inneren anonymen Klassen. Die folgende Klasse SleepInInnerClass gibt nach zwei Sekunden Schlafzeit eine Meldung auf dem Bildschirm aus. Wir starten den Thread dabei aus dem Objekt-Initialisierungsblock. Natürlich hätten wir auch direkt auf der anonymen Unterklasse die Methode start() aufrufen können.

Listing 9.7   SleepInInnerClass.java


public class SleepInInnerClass
{
  public static void main( String args[] )
  {
    new Thread() {
      { start(); }
        {
        public void run() {
          try { sleep(2000); System.out.println("Zeit ist um."); }
          catch ( InterruptedException e ) { }
        } };
  }
}

Galileo Computing

9.3.7 Eine Zeituhr  toptop

Die sleep()-Methode kann auch effektiv zum Warten benutzt werden. Soll ein Programm zu einer bestimmten Zeit eine Aufgabe ausführen, beispielsweise um 18 Uhr eine Nachricht senden, wenn die Simpsons im Fernsehen laufen, so kann ein Thread eingesetzt werden.

Listing 9.8   Clock.java


import java.util.*;

class Clock extends Thread
{
  Calendar cal = new GregorianCalendar();

  public Clock( int timeHour, int timeMinute, String title )
  {
    wakeHour   = timeHour;
    wakeMinute = timeMinute;
    this.title = title;

    start();
  }

  public Clock( int timeHour, String title )
  {
    this ( timeHour, 0, title );
  }

  public void run()
  {
    boolean checked = false;

    while ( true )
    {
      try
      {
        cal.setTime( new Date() );

        if ( cal.get( Calendar.HOUR_OF_DAY ) == wakeHour &&
             cal.get( Calendar.MINUTE ) == wakeMinute )
        {
          if ( !checked )
          {
            // hier nun Arbeit verrichten
            System.out.println( title );

            checked = true;
          }
        }
        Thread.sleep( 1000*10 );   // 10 Sekunden schlafen
      }
      catch ( InterruptedException e ) {};
    }
  }

  private int wakeHour, wakeMinute;
  private String title;

  public static void main( String args[] )
  {
    Clock clock = new Clock( 18, "Simpsons kommt" );
  }
}




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