Weitere aktuelle Java-Titel finden Sie bei dpunkt.
 Inhaltsverzeichnis   Auf Ebene Zurück   Seite Zurück   Seite Vor   Auf Ebene Vor   Eine Ebene höher   Index


11.2

Threads in Java


Ein Thread in Java kann aus verschiedenen Sichten charakterisiert werden. Inhaltlich sind einzelne Threads eine in sich sequenzielle Abfolge von Anweisungen, als Ganzes gesehen laufen sie jedoch parallel zu anderen Threads.

Vom objektorientierten Standpunkt betrachtet ist ein Thread in Java auch ein Objekt. Um einen neuen Thread zu erzeugen, kann man eine Unterklasse der Klasse Thread ableiten, in dieser die Methode run() überschreiben und dann ein Exemplar von dieser neuen Unterklasse erzeugen. Dieses so erzeugte Thread-Objekt kann dann über den Aufruf entsprechender Methoden gestartet und gesteuert werden.

Als Beispiel wollen wir eine Klasse DemoThread von Thread ableiten. Ihre run()-Methode wird so überschrieben, dass sie zehnmal alle fünf Sekunden einen Text ausgibt:
  class DemoThread extends Thread {
  
    public void run() {
      for(int i = 0; i < 10; i++) {
        try {
          sleep(5000);
        }
        catch(InterruptedException e) {
        }
        System.out.println("Demo-Thread");
      }
    }
  
  }
Die Verzögerung zwischen den einzelnen Ausgaben wird mit einem Aufruf der Methode sleep() erzeugt. Ihr wird die Anzahl an Millisekunden übergeben, während der die Ausführung des Threads angehalten wird.

Der sleep()-Aufruf muss in ein try/catch-Statement eingefasst werden, da der Thread während der Wartezeit von einem anderen Thread unterbrochen werden kann. Die Unterbrechung wird ihm durch die Auslösung einer InterruptedException signalisiert.

Im Beispiel ist das Auftreten der InterruptedException irrelevant, daher wird die catch-Anweisung leer implementiert.

Damit der DemoThread in Aktion tritt, müssen zwei Schritte erfolgen: Wenn dieser Thread nun in der main()-Methode einer Applikation erzeugt wird, so läuft die Ausgabe des Texts parallel zum eigentlichen Hauptprogramm der Applikation ab:
  public class ThreadTest {
  
    public static void main(String args[]) {
      DemoThread demoThread;
  
      // Ab jetzt wird "Demo-Thread"
      // im Hintergrund ausgegeben:
      demoThread = new DemoThread();
      demoThread.start();
    }
  }
Auf Einzelprozessor-Maschinen können mehrere Threads nicht wirklich parallel ablaufen. In diesem Fall wird wie bei klassischen Multitasking-Systemen die Prozessorleistung in Abhängigkeit von definierbaren Prioritäten auf die verschiedenen Threads verteilt.

Die parallele Abarbeitung von Programmteilen führt aber auch zu neuen Bedürfnissen und Problemen. Sobald zwei verschiedene Threads auf eine gemeinsam genutzte Ressource, sei es eine Variable, ein Kommunikationskanal oder nur die Standardausgabe, zugreifen wollen, kann es zu Problemen kommen. Das folgende Beispiel zeigt ein Programm mit drei Threads, die alle »parallel« versuchen, einen Text auf der Standardausgabe auszugeben.

Zunächst wird die obige Klasse DemoThread so abgeändert, dass der Ausgabetext dem Konstruktor übergeben wird. Zwischen den einzelnen Textausgaben soll jeder Thread eine kurze Pause zufälliger Länge einlegen, um unterschiedliche Ausführungszeiten zu simulieren, die bei komplexeren Beispielen ohne weiteres auftreten können.
  class TextThread extends Thread {
    String text;
  
    public TextThread(String text) {
      this.text = text;
    }
  
    public void run() {
      for(int i = 0; i < 10; i++) {
        try {
          sleep((int)(Math.random()*1000));
        }
        catch(InterruptedException e) {
        }
       System.out.println(text);
      }
    }
  }
  
  public class TextThreadDemo {
  
    public static void main(String args[]) {
      TextThread java, espresso, capuccino;
  
      java = new TextThread("Java");
      espresso = new TextThread("Espresso");
      capuccino = new TextThread("Cappuccino");
      java.start();
      espresso.start();
      capuccino.start();
    }
  
  }
In der Methode main() des Hauptprogramms werden drei Exemplare mit verschiedenen Texten erzeugt und anschließend gestartet. Die Ausgabe dieses Programms könnte folgendermaßen aussehen:
   Espresso
   Java
   Cappuccino
   Espresso
   Java
   Cappuccino
   Espresso
   Cappuccino
   Cappuccino
   Java
   ...
Wie man sieht, ist die Ausgabe keineswegs gleichzeitig möglich, sondern die einzelnen Threads geben in einer unregelmäßigen Reihenfolge jeweils eine Zeile aus. Das zeitliche Verhalten von parallelen Threads ist also nicht ohne zusätzlichen Aufwand kalkulierbar. In diesem trivialen Beispiel ist dies nicht weiter tragisch. Interessanter wird die Problematik, wenn zwei oder mehrere Threads versuchen, gleichzeitig auf dieselben Datenelemente eines Objekts zuzugreifen. Um den korrekten Ablauf beim gemeinsamen Zugriff auf eine Ressource zu gewährleisten, sind Koordinierungsmaßnahmen erforderlich, die unter dem Begriff Synchronisation zusammengefasst werden.

Material zum Beispiel

Man stelle sich folgendes Beispiel vor: Ein Objekt Bank modelliert eine reale Bank, die Konten für verschiedene Personen verwaltet. Diese hat eine Anzahl von Überweisungen durchzuführen. Da auch in einer realen Bank mehrere Angestellte beschäftigt sind, die sich gleichzeitig um die Bearbeitung solcher Überweisungen kümmern, werden auch im Programmodell die Überweisungen in parallelen Threads abgearbeitet.

In der folgend dargestellten Implementierung wird die Bank durch eine Klasse SimpleBank realisiert. Diese Bank besitzt das Array konten, das die Stände der einzelnen Konten enthält. Der Array-Index soll als Kontonummer dienen. Für Transaktionen zwischen zwei Konten stellt SimpleBank die Methode überweisung() zur Verfügung. Ihr werden die beiden beteiligten Kontonummern sowie der Betrag der Überweisung übergeben.

Zu Demonstrationszwecken wird die Ausführung der Überweisung zwischen der Berechnung des neuen Kontostands und dessen Zurückschreiben unterbrochen. Hierzu wird auf die bekannte Methode sleep() zurückgegriffen. Bisher wurde sleep() aber nur bei der Klasse Thread und ihren Unterklassen verwendet, wohingegen das folgende Beispielprogramm keine explizite Unterklasse von Thread ist. Wie also kann sleep() dann aufgerufen werden?

Die Lösung liegt darin, dass alle Java-Programme auf der Virtual Machine als Threads laufen, auch wenn sie keine expliziten Unterklassen von Thread sind.

Die Methode sleep() wirkt stets auf den Thread, in dem sie aufgerufen wird. Da sie statisch ist, kann man sie auch mit
  Thread.sleep();
aufrufen.

Als letztes verfügt SimpleBank über die Methode kontostand(), mit der die aktuellen Stände aller Konten angezeigt werden können.
  class SimpleBank {
    static int[] konten = {30, 50, 100};
  
    public void überweisung(int von, int nach, int betrag) {
      int neuerBetrag;
  
      neuerBetrag = konten[von];
      neuerBetrag -= betrag;
      // Inkonsistenz, da neuer Betrag noch nicht vermerkt
      try {
        Thread.sleep((int)Math.random()*1000);
      }
      catch(InterruptedException e) {
      }
      konten[von] = neuerBetrag;
  
      neuerBetrag = konten[nach];
      neuerBetrag += betrag;
      // dito
      konten[nach] = neuerBetrag;
    }
  
    public void kontostand() {
      for(int i = 0; i < konten.length; i++)
        System.out.println("Konto "+ i +": " + konten[i]);
    }
  }
Die Überweisungen sollen durch Bankangestellte erfolgen. Diese werden durch die Klasse Angestellter implementiert. Damit mehrere Angestellte gleichzeitig Transaktionen vornehmen können, werden sie von Thread abgeleitet. Jeder Angestellte gehört zu einer Bank. Deshalb wird dem Konstruktor ein Verweis auf SimpleBank übergeben. Über diesen Verweis wird die Methode überweisung() aufgerufen. Die beiden beteiligten Kontonummern sowie der Betrag werden dem Konstruktor übergeben und in entsprechenden Datenelementen der Klasse gespeichert.

Diese Datenelemente werden dann in run() benutzt, um die Methode überweisung() der Bank aufzurufen. Nachdem die Transaktion durchgeführt ist, wird eine aktuelle Kontenübersicht ausgegeben.
  class Angestellter extends Thread {
  
    SimpleBank bank;
    int von, nach, betrag;
  
    public Angestellter(SimpleBank bank, int von,
                               int nach, int betrag) {
      this.bank = bank;
      this.von = von;
      this.nach = nach;
      this.betrag = betrag;
    }
  
    public void run() {
      // Überweisung vornehmen
      bank.überweisung(von, nach, betrag);
      // Kontostand ausgeben
      System.out.println("Nachher:");
      bank.kontostand();
    }
  
  }
Das Demonstrationsprogramm vereinbart drei Verweise auf die Klasse Angestellter. Anschließend wird ein SimpleBank-Objekt erzeugt und eine Übersicht über den Anfangsstand der Konten gegeben. Dann werden die drei Thread-Objekte erzeugt. Die Konten werden hierbei so gewählt, dass sich eine ringförmige Überweisung ergibt. Wenn alles ordnungsgemäß verläuft, dann müsste also die Kontenübersicht am Ende genauso aussehen wie am Anfang.

Schließlich werden die Threads durch Aufrufe ihrer start()-Methoden gestartet.
  public class SimpleBankDemo {
  
    public static void main(String[] args) {
      Angestellter A1, A2, A3;
      SimpleBank b = new SimpleBank();
  
      System.out.println("Vorher:");
      b.kontostand();
  
      // Eine ringförmige Überweisung
      A1 = new Angestellter(b, 0, 1, 20);
      A2 = new Angestellter(b, 1, 2, 20);
      A3 = new Angestellter(b, 2, 0, 20);
  
      A1.start();
      A2.start();
      A3.start();
    }
  
  }
Prinzipiell kann das Beispiel ohne Probleme funktionieren. Falls jedoch von beiden Threads zur gleichen Zeit der Kontostand von Konto A erniedrigt werden soll, so kann es vorkommen, dass beide den gleichen Ausgangskontostand lesen, in ihrer temporären Variablen speichern, davon subtrahieren und dann den neuen Wert schreiben. Je nachdem, ob der erste oder der zweite Thread beim Schreiben schneller ist, wird der Kontostand von Konto A um 10 oder um 20 erniedrigt. Der korrekte Wert wäre jedoch die Summe der Einzelabbuchungen, also 30, gewesen. Die Ausgabe des Programms sieht so (oder ähnlich) aus:
   Vorher:
   Konto 0: 30
   Konto 1: 50
   Konto 2: 100
   Nachher:
   Konto 0: 10
   Konto 1: 70
   Konto 2: 100
   Nachher:
   Konto 0: 10
   Konto 1: 30
   Konto 2: 120
   Nachher:
   Konto 0: 30
   Konto 1: 30
   Konto 2: 80
Wie man der letzten Kontenübersicht entnehmen kann, sind nach der letzten Überweisung 40 EUR »verschwunden«.

Die Ursache des Problems liegt darin, dass der Vorgang der Abbuchung, also das Lesen, Subtrahieren und Schreiben in mehreren Schritten abläuft und so ein zweiter Thread mit einer eigentlich ungültigen Zahl arbeitet. Dieses Verhalten nennt man Race-Condition. Generell sind Race-Conditions Programmfehler, die nur manchmal auftreten, nämlich genau dann, wenn zufällig zwei parallele Threads zur gleichen Zeit auf bestimmte Objekte zugreifen. Derartige Fehler sind in der Praxis schwer zu lokalisieren.

Um das Problem zu umgehen, sollte es eine Möglichkeit geben, diese Sequenz als unteilbare Operation zu definieren oder den Zugriff auf das Konto während der Operation zu sperren. Java sieht hier das Konzept des Monitors vor, mit dem der Zugriff auf gemeinsame Ressourcen geregelt werden kann.

Material zum Beispiel


 Inhaltsverzeichnis   Auf Ebene Zurück   Seite Zurück   Seite Vor   Auf Ebene Vor   Eine Ebene höher   Index

Copyright © 2002 dpunkt.Verlag, Heidelberg. Alle Rechte vorbehalten.