8 Multithreading

Multithreading bedeutet im Wesentlichen, dass mehrere Aufgaben oder Prozesse quasi gleichzeitig ausgeführt werden können. Einfachstes (und bestes) Beispiel für ein Multithreading-System ist ein Mensch, der viele Dinge gleichzeitig erledigt. Bei Computersystemen werden natürliche Dinge wie Multithreading zu einem großen Problem und viele ältere Betriebssysteme wie DOS waren nie in der Lage, Multithreading auch nur ansatzweise auszuführen. Auch Windows 3.x unterstützte nicht einmal echtes Multitasking, geschweige denn Multithreading. Neben den Betriebssystemen haben ebenso die Konzepte bisheriger Programmiersprachen den Multithreading-Vorgang gar nicht oder nur sehr eingeschränkt integriert.

Der Vorgang des Multithreading wird durch die oft verwendete Übersetzung von Threads mit »Aufgaben« ungenau beschrieben und verschleiert den Unterschied zu Multitasking, was ja streng genommen genauso bedeutet, dass mehrere Aufgaben (Tasks) gleichzeitig ausgeführt werden können. Ein Thread ist im Grunde statt mit »Aufgabe« besser (wie es auch im Wörterbuch steht) mit »Faden«, »Faser« oder am besten »Zusammenhang« zu übersetzen. Nicht die quasi gleichzeitige Ausführung von mehreren Programmen wie beim Multitasking, sondern die gleichzeitige, parallele Ausführung von mehreren einzelnen Programmschritten (oder zusammenhängenden Prozessen) ist echtes Multithreading. Dies kann ebenso bedeuten, dass innerhalb eines Programms mehrere Dinge gleichzeitig geschehen, mehrere Fäden/Fasern eines Programms synchron verfolgt und abgearbeitet werden.

Bei Java ist das Multithreadingkonzept voll integrierter Bestandteil der Philosophie. Der hochentwickelte Befehlssatz in Java, um Threads zu erstellen und bei Bedarf zu synchronisieren, ist in die Sprache integriert, macht diese stabil und einfach in der Anwendung. Java verwendet hochintelligente Synchronisationsanweisung für den Umgang mit Multithreading. Die Schlüsselworte synchronized und threadsafe werden zum Markieren von Blöcken und Methoden benutzt, die eventuell vor gleichzeitiger Verwendung geschützt werden sollen.

Grundsätzlich sollte man das Verfahren von Multithreading nicht überschätzen. Gerade auf Singleprozessor-Rechnern und/oder Betriebssystemen, die kein echtes Multithreading unterstützen, bedeutet die Technik nur, dass Prozesse abwechselnd die so genannte Zeitscheibe des Prozessors bekommen. Echte Gleichzeitigkeit ist damit nicht möglich.

8.1 Klassen Thread-fähig machen

Es gibt zwei Wege, wie Sie Teile Ihrer Anwendungenen und Klassen in separaten Threads laufen lassen können. Einmal, indem Sie die Thread-Klasse erweitern oder indem Sie die Schnittstelle Runnable implementieren. Beide finden Sie im Paket java.lang. Damit stehen sowohl die Klasse als auch die Schnittstelle überall zur Verfügung.

8.1.1 Die Erweiterung der Klasse Thread

Sie können eine Klasse als Thread lauffähig machen, indem Sie die Klasse java.lang.Thread erweitern. Dies gibt Ihnen direkten Zugriff auf alle Thread-Methoden.

Beispiel:

public class MeineThreadKlasse extends Thread

In der als Thread lauffähig gemachten Klasse müssen Sie die Methode

public void run()

überschreiben. Aus der so vorbereiteten Klasse erzeugen Sie dann mit einem der zahlreichen Konstruktoren (Thread(), Thread(Runnable target), Thread(Runnable target, String name), Thread(String name), Thread(ThreadGroup group, Runnable target), Thread(ThreadGroup group, Runnable target, String name), Thread(ThreadGroup group, String name)) ein Thread-Objekt.

Wir wenden verschiedene der Konstruktoren im Laufe des Kapitels an.

Start von Threads

Wenn Sie dann aus dieser Klasse ein Objekt erzeugen, können Sie den Thread über diese Objektrepräsentation und die Methode start() laufen lassen1. Die Methode wirft die Ausnahme vom Typ IllegalThreadStateException aus, wenn der Thread bereits läuft.

Die Methode run() sollte nie direkt aufgerufen werden. In diesem Fall wird das als normaler Methodenaufruf interpretiert und nicht als Start eines Threads.

Schauen wir uns ein Beispiel an.

class ErsterThread extends Thread {
 long erg;
 public void run() {
  while(true) {
   System.out.println(erg++);
 }   }  }
class ZweiterThread extends Thread {
 long erg;
 public void run() {
  while(true)  {
   System.out.println(erg--);
 }   }  }
public class ThreadTest1 {
  public static void main(String args[])  {
  ErsterThread p = new ErsterThread();
  ZweiterThread q = new ZweiterThread();
  p.start();
  q.start();
 }  }

Das Programm lässt sich nur durch die Tastenkombination STRG+C in der DOS-Box abbrechen

In dem Beispiel arbeiten wir mit zwei Thread-fähig gemachten Klassen, die beide die run()-Methode überschreiben. Dort läuft jeweils eine Endlosschleife. Eine zählt eine Variable hoch und gibt sie aus, die andere zählt die Variable herunter. In der eigentlichen Applikation erzeugen wir jeweils ein Objekt und starten dann die beiden Threads. Beachten Sie, dass damit die main()-Methode beendet ist und ein nicht Multithreading-fähiges Programm unmittelbar beendet werden würde. Insbesondere würde es aber auch gar nicht zum Aufruf von q.start() kommen, denn die erste Endlosschleife würde das verhindern. So aber werden sie sehen, dass beide Threads gestartet werden und die Ausgabe zwischen positiven und negativen Zahlen hin- und herwechselt (je nachdem, welcher Thread gerade die Ausgabe erzeugt).

Threads abbrechen

Ein Thread wird beendet, wenn das Ende seiner run()-Methode erreicht ist oder das umgebende Programm endet.

Es gibt aber auch die Möglichkeit, einen Thread über einen Stopaufruf an sein Objekt zu beenden oder zumindest zu unterbrechen. Die mittlerweile als deprecated geltende Methode stop() kann das tun (wenngleich mit nicht vorhersagbarem Ausführungszeitpunkt). Testen wir die Methode.

class DritterThread extends Thread {
 long erg;
 public void run() {
  while(true)  {
   System.out.println(erg++);
 }   }  }
class VierterThread extends Thread {
 DritterThread p = new DritterThread();
 public void run() {
  p.start();
  while(true)  {
   if(p.erg>100) {
    p.stop();
    break;
 }  }  }  }
public class ThreadTest2 {
  public static void main(String args[])  {
  VierterThread q = new VierterThread();
  q.start();
 }  }

Das Beispiel unterscheidet sich nur auf den ersten Blick nicht sonderlich von dem ersten. Sie sind aber grundverschieden. Zwar arbeiten wir auch hier mit zwei Thread-fähigen Klassen, aber nur die erste beinhaltet eine Endlosschleife ohne interne Abbruchmöglichkeit. In der zweiten Klasse wird der Thread der ersten Klasse erzeugt und im Rahmen der run()-Methode gestartet (nicht im Hauptprogramm). Die zweite Thread-Klasse hat die Aufgabe, in einem eigenen, parallel laufenden Thread zu überwachen, wann der erste Thread abgebrochen werden soll. Im Rahmen der eigentlichen Applikation wird dann aus dieser zweiten Klasse ein Thread-Objekt erzeugt und nur dieses gestartet. Der erste Thread zählt wieder eine Variable hoch, der zweite Thread durchläuft permanent eine if-Schleife, in der kontrolliert wird, ob dieser Wert größer als 100 ist. In diesem Fall wird die stop()-Methode für den ersten (!) Thread ausgelöst und mit einem break die Endlosschleife des zweiten Threads beendet. Damit endet auch automatisch seine run()-Methode. Das Verfahren funktioniert soweit zuverlässig, nur wird Ihnen bei einem Test auffallen, dass die Zählvariable des ersten Threads beim Abbruch bedeutend größer als 100 sein wird und vor allem, dass der Wert bei jedem Test verschieden sein wird. Wie bereits angedeutet, hat man auf diese Art und Weise keine genaue Kontrolle über den Zeitpunkt des Abbruchs. Außerdem ist unter gewissen Umständen die gesamte Technik mit der stop()-Methode unsicher. Die Methode gilt deshalb als deprecated. Dennoch - für zahlreiche Anwendungen ist diese einfache Technik immer noch sinnvoll.

Es gibt aber mittlerweile eine bessere Variante, die wir in dem nachfolgenden, umgebauten Beispiel einsetzen wollen. Sie basiert auf den folgenden drei Methoden:

interrupt()
interrupted()
isInterrupted()

Diese Technik basiert auf einem Unterbrechungsstatus, der gesetzt werden kann und dann in Form von Botschaften an einen Thread weitergereicht wird. Dieser nimmt den Status, wenn der Thread wieder vom Prozessor die Zeitscheibe bekommt, unmittelbar zur Kenntnis und man kann entsprechende Aktionen auslösen. Die Methode

public void interrupt()

unterbricht den Thread, auf den sie angewendet wird. Über die Methode

public static boolean interrupted()

kann man testen, ob ein aktueller Thread unterbrochen wurde (falls ja, Rückgabe true). Der Aufruf der Methode löscht den Interrupted-Status von dem Thread, sodass ein zweiter Aufruf false zurückliefern wird (es sei denn, der Thread ist zwischenzeitlich erneut unterbrochen worden). Die Methode

public boolean isInterrupted()

testet ebenso, ob ein Thread unterbrochen wurde, nur wird der Interrupted-Status bei einem Aufruf unverändert gelassen.

Bei allen drei Methoden sollte man beachten, dass man damit nur mit der Unterbrechung eines Threads agiert, ihn aber noch nicht explizit beendet. Darum muss man sich noch selbst kümmern. Der Unterbrechungsstatus ermöglicht jedoch auf einfache Weise, eine Abbruchanweisung im unterbrochenen Thread auszulösen, etwa wie in dem folgenden Beispiel.

class FuenfterThread extends Thread {
 long erg;
 public void run() {
 while(true) {
 System.out.println(erg++);
 if(isInterrupted()) {
 System.out.println("Erster Thread unterbrochen");
 break;
 }  }  }  }
class SechsterThread extends Thread {
 FuenfterThread p = new FuenfterThread();
 public void run() {
 p.start();
 while(true) {
 if(p.erg>100) {
 p.interrupt();
 break;
 }  }  }  }
public class ThreadTest3 {
 public static void main(String args[]) {
 SechsterThread q = new SechsterThread();
 q.start();
 }  }

Zwei mittlerweile als deprecated erklärte Methoden sind suspend() und resume(). Diese dienten ursprünglich dazu, einen Thread anzuhalten und wieder zu starten. suspend() versetzt einen Thread in Wartezustand, die Methode resume() kehrt zu dem schlafenden Thread zurück und aktiviert ihn wieder. Sie sollten nicht mehr verwendet werden, da damit leicht ein Deadlock erzeugt werden kann.

Von Interesse ist in diesem Zusammenhang die Methode public final boolean isAlive(), womit getestet werden kann, ob ein Thread beendet wurde. Falls er noch nicht beendet wurde, wird true zurückgegeben. Eine weitere Methode zur Änderung des Laufstatus von Threads ist public void destroy(). Im Allgemeinen sollten Sie destroy() jedoch nicht anwenden, denn damit werden keine Säuberungen am Thread durchgeführt, sondern er wird lediglich zerstört.

Threads schlafen legen

Eine sehr interessante Methode ist sleep(), die es in zwei Versionen gibt:

static void sleep(long millis) 
static void sleep(long millis, int nanos)

Damit kann man einen Thread für ein in Millisekunden und optional Nanosekunden angegebenes Zeitintervall »schlafen legen«.

Diese Zeitangaben sind aber (gerade auf einem Singleprozessorsystem) nur sehr ungenau und können von diversen Faktoren abhängen (Betriebssystem, Hardware, parallel laufende Programme usw.).

Die Methode ist eine Klassenmethode und kann deshalb auch ohne konkretes Thread-Objekt angewandt werden. Dies kann dazu verwendet werden, das Hauptprogramm (was ja selbst auch als eigener Thread zu verstehen ist) selbst eine gewisse Zeit pausieren zu lassen. Testen wir die Methode.

import java.util.*;
class SiebterThread extends Thread {
 int a;
 public void run() {
 while(a<20) {
 Date d = new Date();
 System.out.println(d.toString());
 a++;
 try {
 sleep(5000);
 }
 catch( InterruptedException e) {
 System.out.println(e.getMessage());
 }  }  }  }
public class ThreadTest4 {
 public static void main(String args[]) {
 SiebterThread q = new SiebterThread();
 q.start();
 }  }

Abbildung 8.1:  Alle 5 Sekunden arbeitet der Thread

Die Methode wirft eine Ausnahme vom Typ InterruptedException aus und muss entsprechend in einen passenden try-catch-Block eingeschlossen werden.

In einem ähnlichen Zusammenhang wie sleep() ist die join()-Methode (public final void join() throws InterruptedException) zu sehen. Es gibt aber eine zusätzliche Funktionalität. Damit können Sie gezielt eine angegebene Zeitspanne auf die Beendigung eines Threads warten. So etwas ist immer dann sinnvoll, wenn es zeitaufwändige Operationen gibt, die vor dem Aufruf einer Folgeoperation noch beendet werden müssen. Dieser zeitaufwändigen Operation wird sinnvollerweise ein eigener Thread zugeordnet. Die join()-Methode lässt sich auch als Alternative zu der sleep()-Methode verwenden und unterstützt wie diese eine Wartezeit in Millisekunden oder Millisekunden und Nanosekunden:

public final synchronized void join(long millis) throws InterruptedException 
public final synchronized void join(long millis, int nanos) throws InterruptedException

Java stellt auch diverse Methoden bereit, mit denen man beispielsweise die Anzahl der laufenden Threads ermitteln (public static int activeCount()) oder deren Namen abfragen (public final String getName()) bzw. setzen (public final void setName(String name)) kann. Um Namen für Threads zu setzen, gibt es die Möglichkeit, diese im Konstuktor der Thread-Klasse als String anzugeben. Falls der Konstruktor mit leeren Klammern verwendet wird, wird ihm standardmäßig ein Standardname Thread-x zugewiesen, wobei x eine eindeutige Nummer für diesen Thread ist und ab 0 indiziert wird.

Ganz wichtig ist auch die Methode zum Abfragen des derzeit ausgeführten Threads:

public static Thread currentThread()

Schauen wir uns die Methoden in einem praktischen Beispiel an.

class ThreadKlasse extends Thread {
 long erg;
 public void run() {
  while(true) {
   erg++;
   System.out.println(getName());
   System.out.println(activeCount());
   System.out.println(currentThread());
 }  }  }
public class VerschiedeneThreadMethoden {
  public static void main(String args[]) {
  ThreadKlasse p = new ThreadKlasse();
  ThreadKlasse q = new ThreadKlasse();
  ThreadKlasse r = new ThreadKlasse();
  ThreadKlasse s = new ThreadKlasse();
  s.setName("Mein Thread, du lauefst so stille");
  p.start();
  q.start();
  r.start();  
  s.start();
  }  }

In dem Beispiel setzen wir explizit den Namen eines Threads. Innerhalb der run()-Methode geben wir die Namen des gerade aktiven Threads, die Anzahl aller laufenden Threads (immer konstant 5 in dem Beispiel - das Hauptprogramm zählt mit) und das gerade aktive Thread-Objekt aus.

Abbildung 8.2:  Die Ausgabe gibt diverse Informationen über Anzahl, Name und Aktivität der Threads.

Die Priorität verändern

Threads bekommen vom System bestimmte Prioritäten zugewiesen, was gleichbedeutend mit der zur Verfügung gestellten Prozessorzeit ist. Die Defaultpriorität (5) kann verändert und damit festgelegt werden, dass die höher priorisierten Threads schneller abgearbeitet werden. Dazu steht die Methode public final void setPriority(int newPriority) throws IllegalArgumentException zur Verfügung. Diese Methode erlaubt es, die Priorität für einen Thread mit Werten zwischen 1 (niedrig) und 10 (hoch) festzulgen. Andere Angaben lösen die Ausnahme IllegalArgumentException aus.

Die Partnermethode public final int getPriority() wird benutzt, um die aktuelle Priorität eines Threads zu ermitteln.

class ErsterThread extends Thread {
 int erg;
 public void run() {
  System.out.println("Prioritaet von Thread 1: " 
  + getPriority());
  while(erg < 100)  {
   System.out.print(erg++ + ", ");
 }  }  }
class ZweiterThread extends Thread {
 int erg;
 public void run() {
  System.out.println("Prioritaet von Thread 2: " 
  + getPriority());
  while(erg < 100) {
   System.out.print(200 + erg++ + ", ");
 }  }  }
public class ThreadPrio1 {
  public static void main(String args[]) {
  ErsterThread p = new ErsterThread();
  ZweiterThread q = new ZweiterThread();
  p.start();
  q.start();
 }  }

Das Beispiel zeigt, das beide Threads die Defaultpriorität 5 haben. Durch die schnell zu erledigende Aufgabe wird erst der erste Thread vollständig abgearbeitet und dann der zweite.

Abbildung 8.3:  Bei gleicher (Default-)Priorität wird zuerst der erste und dann der zweite Thread abgearbeitet.

Wenn das Beispiel geringfügig verändert wird, kann man die Reihenfolge der Abarbeitung beeinflussen.

class ErsterThread_a extends Thread {
 int erg;
 public void run() {
  System.out.println(
  "Anfangs-Prioritaet von Thread 1: " 
  + getPriority());
  setPriority(4);
  System.out.println(
  "Neue Prioritaet von Thread 1: " 
  + getPriority());
  while(erg < 100) {
   System.out.print(erg++ + ", ");
 }  }  }
class ZweiterThread extends Thread {
 int erg;
 public void run() {
  System.out.println("Prioritaet von Thread 2: " + getPriority());
  while(erg < 100) {
   System.out.print(200 + erg++ + ", ");
 }  }  }
public class ThreadPrio2 {
  public static void main(String args[]) {
  ErsterThread_a p = new ErsterThread_a();
  ZweiterThread q = new ZweiterThread();
  p.start();
  q.start();
 }  }

Die Reduzierung der Priorität von Thead 1 führt dazu, dass unmittelbar nach dessen Start die Zeitscheibe zu Thread 2 weitergegeben wird, dieser abgearbeitet und erst dann mit Thread 1 weiter gemacht wird. Das äußert sich auch darin, dass die zweite Ausgabe der Priorität von Thread 1 erst nach der Abarbeitung von Thread 2 erfolgt.

Abbildung 8.4:  Thread 2 hat eine höhere Priorität und wird zuerst beendet.

Die Variante mit der Erweiterung der Klasse Thread blockiert den Weg der Vererbung und ist deshalb in einigen Situationen nur bedingt sinnvoll. Für Applets scheidet sie sowieso aus, denn diese müssen ja von java.applet.Applet abgeleitet werden. Deshalb gibt es einen zweiten, in diesem Fall sinnvolleren Weg - die Implementation von Runnable.

8.1.2 Implementation von Runnable

Wenn Sie eine Klasse in die Lage versetzen, Threads laufen zu lassen, werden Sie unter Umständen auch die Fähigkeiten einiger anderer Klassen erweitern wollen. Da Java keine Mehrfachvererbung unterstützt, können Sie für die Multithreadingfähigkeit das Alternativkonzept der Schnittstellen verwenden. Sie implementieren einfach die Schnittstelle Runnable. Tatsächlich implementiert sogar die Klasse Thread selbst Runnable. Die Runnable-Schnittstelle besitzt nur eine Methode: run().

Diese Schnittstelle kennen wir bereits aus unserem Multithreading-Applet im letzten Kapitel. Jedesmal wenn Sie in eine Klasse Runnable implementieren, werden Sie die run()-Methode in Ihrer Klasse überschreiben (müssen). Es ist dann die Methode, die all diejenige Arbeit erledigt, die Sie von einem oder mehreren Thread(s) getan haben wollen.

Beispiel:

public class DatumThread extends java.applet.Applet implements Runnable

Grundsätzlich unterscheidet sich dabei die Vorgehensweise zwischen eigenständigen Applikationen und Applets nicht allzu sehr. Um Applets Multithreading-fähig machen, ist hier nicht viel zu beachten, was nicht auch für beliebige Klassen gilt.

Wir haben im vorherigen Kapitel über die Grundlagen der Applet-Erstellung bereits gesehen, dass ein Applet im Wesentlichen über vier Schritte Multithreading-fähig gemacht wird. Das übertragen wir nun auf den allgemeinen Fall:

  • Erweitern der Unterschrift der Klasse um implements Runnable.
  • Hinzufügen einer Instanzvariablen, der der Thread zugewiesen wird.
  • Bei einem Applet, das selbst die Schnittstelle implementiert, wird die start()-Methode so reduziert, dass sie außer dem Start des Threads und einer ggf. notwendigen Erzeugung eines Thread-Objekts keinen weiteren Aufruf enthält. Wenn eine eigenständige Applikation die Schnittstelle implementiert, gilt das Gleiche für die main()-Methode.
  • Hinzufügen der run()-Methode, die den eigentlichen Code enthält, welchen der Thread ausführen soll.

Die Variante, bei der ein Applet die Schnittstelle Runnable direkt implementiert, haben wir bereits im letzten Kapitel durchgespielt. Hier folgt eine Möglichkeit, wie ein Applet diese Schnittstelle nicht direkt implementiert, sondern eine externe Klasse verwendet, die diese implementiert (wobei diese auch die Klasse Thread erweitern könnte).

import java.awt.*;
import java.applet.*;
import java.util.*;
class ThreadEins implements Runnable {
 Date d; 
 public void run() {
  while(true) {
   d = new Date();
   System.out.println(d);
 }  }  }
public class AppletThread1 extends Applet {
  Thread q;
  public void start() {
  ThreadEins p = new ThreadEins();
  q = new Thread(p);
  q.start();
 }  }

Das Beispiel erzeugt eine Ausgabe in der DOS-Box (die jeweils aktuelle Zeit samt Datum), sobald die HTML-Seite, in der das Applet integriert ist, mit dem Appletviewer aufgerufen wird. Beachten Sie, dass der Thread-Kontruktor ein Objekt der Appletklasse übergeben bekommt. Erstellen wir nun noch ein kleines Beispiel, in dem eine vollständige Applikation die Schnittstelle Runnable direkt in der Hauptklasse implementiert.

public class ThreadTest5 implements Runnable {
 int a;
 public void run() {
  while(a<100)  {
   System.out.print(a + ", ");
   a++;
 }  }
  public static void main(String args[])  {
  ThreadTest5 a = new ThreadTest5();
  Thread q = new Thread(a);
  q.start();
 }  }

Beachten Sie auch bei diesem Beispiel, dass das Objekt, das aus der Klasse selbst erzeugt wurde, als Parameter an den Konstruktor der Klasse Thread() übergeben wird.

Im Allgemeinen ist es bei eigenständigen Applikationen sinnvoller, wenn jeder Thread in einer eigenen Klasse definiert wird und diese dann die Schnittstelle Runnable implementiert oder von der Klasse Thread abgeleitet wird. Das ist übersichtlicher. In der Applikation wird dann ein Objekt dieser Klasse erzeugt und der Thread nur gestartet. Bei Applets ist die direkte Implementation der Schnittstelle Runnable meist die glücklichere Wahl.

Mehr Beispiele dazu sollen in Verbindung mit dem nachfolgenden Thema erfolgen. Es handelt sich um die Gruppierung von Threads.

8.2 Thread-Gruppen

Threads können in so genannte Thread-Gruppen eingeteilt werden, die im Wesentlichen eine Sicherheitsstrategie für Threads festlegen. Sie können damit vermeiden, dass ein Thread andere Threads ruhig stellt oder deren Reihenfolge ändert. Thread-Gruppen werden hierarchisch so angeordnet, dass jede Thread-Gruppe eine Ursprungsgruppe (parent group) hat. Ein Thread darf nur Threads innerhalb seiner eigenen Thread-Gruppe oder einer seiner Untergruppen verändern.

Über die Klasse ThreadGroup, die zu dem Paket java.lang gehört, können Sie die Gruppierung von Threads vornehmen. So ermöglicht es eine Gruppierung auch, verschiedene Operationen auf mehrere Threads gemeinsam anzuwenden.

8.2.1 Eine Thread-Gruppe erstellen

Sie können eine Thread-Gruppe ganz einfach über den Konstruktor

ThreadGroup(string groupName)

erstellen. Sie können eine Thread-Gruppe ebenso als Untergruppe einer bereits existierenden Thread-Gruppe erstellen:

public ThreadGroup(ThreadGroup existingGroup, String groupName) throws NullPointerException

Viele der ThreadGroup-Operationen sind, solange sinnvoll, mit normalen Thread-Operationen identisch, außer, dass sie für alle Threads ihrer Gruppe gültig sind.

Die maximale Priorität eines jeden Threads einer Gruppe können Sie durch das Anwenden der Methode

public final synchronized void setMaxPriority(int priority)

auf die Gruppe beschränken und mit

public final int getMaxPriority()

abfragen.

Die übergeordnete Gruppe einer Thread-Gruppe können Sie mit

public final ThreadGroup getParent()

herausfinden. Dabei wird neben dem Namen auch die maximal erlaubte Priorität der übergeordneten Gruppe angegeben.

Die unterschiedlichen enumerate()-Methoden ermöglichen es Ihnen herauszufinden, welche Threads und Thread-Gruppen zu einer bestimmten Thread-Gruppe gehören:

public int enumerate(Thread[] threadListe) 
public int enumerate(Thread[] threadListe, boolean rekursivEinstellung) 
public int enumerate(ThreadGroup[] groupListe) 
public int enumerate(ThreadGroup[] groupListe, boolean rekursivEinstellung)

Der Parameter rekursivEinstellung der enumerate()-Methoden sorgt dafür, dass die enumerate()-Methode bei dem Wert true all seine Untergruppen durchforstet, um eine komplette Liste seiner Nachkommen zu erhalten.

Die Anzahl der aktiven Threads und Thread-Gruppen einer Gruppe erhalten Sie durch die Verwendung von

public synchronized int activeCount()

und

public synchronized int activeGroupCount().

8.2.2 Hinzufügen eines Threads zu einer Thread-Gruppe

Wenn Sie einen Thread einer Thread-Gruppe hinzufügen wollen, müssen Sie diese bei seiner Erstellung mit einem alternativen Konstruktor angeben:

public Thread(ThreadGroup group, String name) 
public Thread(ThreadGroup group, Runnable target, String name)

Über den optionalen Parameter name können Sie einen Thread-Namen vergeben, der zum Unterscheiden der einzelnen Threads verwendet werden kann. Wenn der Thread nicht benannt wird, kann der Thread-Name null angegeben werden. Das Attribut target im zweiten Konstruktor gibt das Objekt an, dessen run()-Methode beim Start des Threads aufgerufen werden soll. In einigen Situationen ist das notwendig.

Testen wir Thread-Gruppen in einem Beispiel.

public class ThreadGruppe extends Thread {
 int erg=1;
 public void run() {
  while(erg<10000)  {
  erg++;
 }   }
  public static void main(String args[])  {
  ThreadGroup grp1 = new ThreadGroup("Haupt");
  ThreadGroup grp2 = 
  new ThreadGroup(grp1, "Neben1");
  ThreadGroup grp3 
  = new ThreadGroup(grp1, "Neben2");
  ThreadGruppe a = new ThreadGruppe();
  Thread p = new Thread(grp1,a,"Eins");
  Thread q = new Thread(grp1,a,"Zwei");
  Thread r = new Thread(grp2,a,"Drei");
  Thread s = new Thread(grp2,a,"Vier");
  Thread t = new Thread(grp3,a,"Fuenf");
  Thread u = new Thread(grp3,a,"Sechs");
  Thread v = new Thread(grp3,a,"Sieben");
  grp1.setMaxPriority(9);
  grp2.setMaxPriority(6);
  grp3.setMaxPriority(4);
  System.out.println(
  "Uebergeordnete Gruppe grp1: "  + 
  grp1.getParent());
  System.out.println(
  "Uebergeordnete Gruppe grp2: " + 
  grp2.getParent());
  System.out.println(
  "Uebergeordnete Gruppe grp3: " +
   grp3.getParent());
  System.out.println("Max Prioritaet grp1: " + 
  grp1.getMaxPriority());
  System.out.println("Max Prioritaet grp2: " + 
  grp2.getMaxPriority());
  System.out.println("Max Prioritaet grp3: " + 
  grp3.getMaxPriority());
  System.out.println("Aktive Threads grp1: " + 
  grp1.activeCount());
  System.out.println("Aktive Threadgruppen grp1: " 
  + grp1.activeGroupCount());
  System.out.println("Aktive Threads grp2: " + 
  grp2.activeCount());
  System.out.println("Aktive Threadgruppen grp2: " 
  + grp2.activeGroupCount());
  System.out.println("Aktive Threads grp3: " + 
  grp3.activeCount());
  System.out.println("Aktive Threadgruppen grp3: " 
  + grp3.activeGroupCount());
  p.start();
  q.start();
  r.start();
  s.start();
  t.start();
  u.start();
  v.start();
  }    
}

Das Beispiel benutzt eine einzige Klasse, die Thread-fähig ist. Diese tut nicht viel. Es wird nur eine Variable hoch gezählt. Aus dieser Klasse werden sieben Threads erstellt, die verschiedenen Thread-Gruppen zugeordnet werden. Es gibt eine Hauptgruppe, die wiederum zwei Untergruppen beinhaltet. Für die Thread-Gruppen werden maximal erlaubte Prioritäten festgelegt.

Abbildung 8.5:  Threads zu Gruppen zusammengefasst

8.3 Dämonen

Vielleicht werden Sie sich fragen, was Java mit einem übernatürlichen Wesen bzw. einer übernatürlichen Macht - nicht notwendigerweise böse - zu tun hat. Nun, Java-Threads können einem von zwei Typen angehören:

  • Benutzer-Threads
  • Dämon-Threads

Der Name Dämon-Thread bzw. Daemon-Thread stammt einigen Quellen zu Folge aus der UNIX-Welt und könnte die Abkürzung für »Disk And Execution Monito>« sein. Darauf deutet ebenso die Aufgabe hin, für die Dämonen hauptsächlich verwendet werden.

Die Dämon-Threads haben einige nette Eigenschaften, die sie gegenüber Benutzer-Threads für manche Aufgaben priorisieren. Sie müssen sich beispielsweise niemals darum kümmern, ob ein einmal ins Rennen geschickter Dämon-Thread wieder zurückkommt. Nachdem ein Dämon-Thread gestartet wurde, muss er auch nicht gestoppt werden. Wenn der Thread das Ende seiner Aufgabe erreicht, wird er automatisch stoppen und sein Status wird deaktiviert.

Ein weiterer sehr wichtiger Unterschied (und der wahrscheinliche Grund für den Namen) zwischen Dämon-Threads und Benutzer-Threads ist der, dass Dämon-Threads die ganze Zeit laufen können. Wenn der Java-Interpreter feststellt, dass nur noch Dämon-Threads laufen, beendet er seine Ausführung, ohne sich darum zu kümmern, ob die Dämon-Threads fertig sind. Das ist sehr nützlich, da es gestattet, Threads zu starten, die Dinge wie Verwaltungsaufgaben, Beobachtung und Säuberungen im Hintergrund ausführen; sie werden von alleine sterben, wenn nichts anderes mehr läuft. Normalerweise endet ein Java-Programm erst, wenn alle seine (normalen) Threads beendet sind2. Die Dämon-Eigenschaft veranlasst die virtuelle Maschine von Java, diese Threads beim Überprüfen aller offenen Threads vor Beendigung eines Programms zu ignorieren.

Die Nützlichkeit dieser Dämonen-Technik war in früheren Java-Versionen weitgehend auf grafische Java-Anwendungen beschränkt, da per Standard einige der Basis-Threads nicht als Dämon definiert sind. Dazu gehören AWT-Input, AWT-Motif, Main und Screen-Updater. Dies bedeutet, dass eine Anwendung, die die AWT-Klasse benutzte, keine Dämon-Threads besitzt, sondern Threads, die die Anwendung daran hindern, sauber beendet zu werden, bevor alle Threads beendet sind.

Ab Java 2 und dem JDK 1.2 haben Dämonen eine beträchtliche Aufwertung erfahren. Die Erweiterung des RMI-Konzepts erlaubte es nun, Objekte anhand einer Referenz zu reaktivieren, wenn diese zuvor persistent gemacht wurden (Remote Object Activation). Die Umsetzung erfolgt mit dem neu im JDK 1.2 eingeführten Tool rmid - Java RMI Activation System Daemon. Dieses Tool startet den so genannten Activation System Daemon, einen speziellen Dämon, mit dessen Hilfe Objekte in einer JVM registriert und aktiviert werden können.

Die intensive Behandlung von RMI und dem Umfeld sprengt den Rahmen dieses Buchs, aber Sie finden dazu in der Dokumentation des Tools im Anhang eine umfangreiche Anleitung. Zudem wird im Kapitel über die erweiterten Java-Techniken auf RMI und verwandte Techniken eingegangen.

Zwei Methoden sind im Wesentlichen für den Dämonen-Status eines Threads zuständig. Dies sind:

public final boolean isDaemon() 
public final void setDaemon(boolean on)

Die Methode isDaemon() wird benutzt um den Status eines bestimmten Threads zu testen. Gelegentlich ist dies für ein als Thread laufendes Objekt nützlich um festzustellen, ob es als Dämon oder als regulärer Thread läuft. Die Methode liefert true zurück, wenn der Thread ein Dämon ist und false im anderen Fall.

Die Methode setDaemon(boolean) wird benutzt, um den Dämonen-Status eines Threads zu verändern. Um einen Thread zu einem Dämon zu machen, setzen Sie den Eingangswert auf true. Um wieder zurück zu einem Benutzer-Thread zu wechseln, setzen Sie den Wert auf false.

Wenn Sie in einem Dämon-Thread einen anderen Thread starten, wird auch dieser beendet, wenn der Dämon-Thread beseitigt wird (auch ein Benutzer-Thread).

Lassen Sie uns ein kleines Beispiel durchspielen, das auf einem von uns schon benutzten Exempel basiert. Es geht um das Beispiel, in dem ein Thread eine Endlosschleife enthält, die von einem zweiten Thread beobachtet und unterbrochen wird, wenn eine Zählvariable einen gewissen Wert überschreitet.

class KeinDaemon extends Thread {
 long erg;
 public void run() {
  while(true)  {
  // System.out.print(erg++);
   erg++;
   if(isInterrupted()) {
   // KEIN ABBRUCH
   System.out.println("Thread p unterbrochen " 
  + erg);
 }  }  }  }
class EinDaemon extends Thread {
 KeinDaemon p = new KeinDaemon();
 public void run() {
  System.out.println("Ist p Daemon? " 
  + p.isDaemon());
  System.out.println("Ist aktiver Thread Daemon? " 
  + this.isDaemon());
  // p wird zum Daemon
  p.setDaemon(true);
  System.out.println("Ist p Daemon? " 
  + p.isDaemon());
  p.start();
  while(true) {
   if(p.erg>50) {
    p.interrupt();
    break;
 }  }  }  }
public class Luzifer {
  public static void main(String args[]) {
  EinDaemon q = new EinDaemon();
  q.setDaemon(false);
  q.start();
 }  }

In dem Beispiel wird der Thread p explizit als Dämon deklariert, der von dem Benutzer-Thread q aufgerufen wird. Irgendwann unterbricht q den Thread p. Dieser Thread p wird aber explizit nicht (!) beendet, sondern der Benutzer-Thread q. Wenn p ein Benutzer-Thread wäre, müsste er auf jeden Fall noch explizit beendet werden. Da es sich jedoch um einen Dämon handelt, wird der Interpreter trotz aktivem Thread das Programm beenden (allerdings erst nach einigen nicht genau vorhersehbaren Zwischenschritten, wie die Ausgabe zeigt - darum geht es aber bei der Demonstration nicht).

Abbildung 8.6:  Der Benutzer-Thread wird beendet und »zieht den Dämon mit  ins Grab«.

8.4 Schutzmaßnahmen bei Multithreading

Multithreading setzt eine gewisse Umsicht und gegebenenfalls auch Vorsichtsmaßnahmen voraus. Da innerhalb eines Programms verschiedene Prozesse parallel laufen, kann es durchaus passieren, dass diese gleichzeitig auf Ressourcen (etwa einen Datensatz in einer Datenbank) zuzugreifen versuchen oder ein Prozess fertig ist und einen anderen, noch nicht bereiten Prozess nun benötigt. In vielen Multithreading-Applikationen müssen Vorgänge synchronisiert werden oder auch Prozesse ein wenig »an die Kandare« genommen werden, damit sie anderen Prozessen nicht in die Quere kommen. Java stellt dazu einige ausgefeilte Techniken bereit.

8.4.1 Methoden synchronisieren

Methoden zu synchronisieren bedeutet, eine Datenverletzung zu verhindern, die passieren kann, wenn zwei Methoden gleichzeitig versuchen, auf dieselben Daten zuzugreifen. So etwas kann durchaus vorkommen, wenn Sie mit Multithreading arbeiten. Dies ist ähnlich dem Fall, wo zwei oder mehr Anwender in einer Netzwerkdatenbank den gleichen Datensatz allokieren und dann verändern wollen. Indem Sie in Java das Schlüsselwort synchronized vor eine Methodendeklaration setzen, können Sie die Datenverletzungen verhindern.

Beispiel: synchronized void toggleStatus()

Um Datenverletzungsprobleme zu vermeiden, deklarieren Sie am besten alle betreffenden Methoden als synchronized. In einem bestimmten Objekt dürfen keine zwei synchronisierten Methoden gleichzeitig laufen. Wenn eine Methode aufgerufen wird, erhält Sie ein »Schloss« für das Objekt. Wenn dann eine weitere synchronisierte Methode aufgerufen wird, muss die zweite solange warten, bis die erste fertig ist und das Schloss für dieses Objekt wieder geöffnet wurde. Auf diese Weise können Sie Zugriffsprobleme verhindern, wie sie durch paralleles Laufen von zwei Threads entstehen können, die beide gleichzeitig auf die gleichen Daten zugreifen wollen. Wenn Sie also den Modifier synchronized vor beide Methoden setzen, kann keine von beiden gleichzeitig mit der anderen laufen.

8.4.2 Vorsicht, Deadlock!

Dabei ist indes eine gewisse Sorgfalt angebracht, weil sich mehrere synchronisierte Methoden auch gegenseitig blockieren können, indem die eine Methode auf ein Ereignis wartet, das nur von der synchronisierten und deshalb gerade blockierten Methode abhängt.

Sie kennen vielleicht das berühmte Beispiel von den drei Chinesen mit dem zwei Paar Essstäbchen, das in verschiedenen Varianten erzählt wird. Es handelt sich aber immer um eine Situation, die der folgenden Beschreibung ähnlich ist:

Die drei Chinesen sitzen zum Essen um einen Tisch und haben nur zwei Paar Essstäbchen zur Verfügung. Jeder bekommt also nur ein Stäbchen. Das vierte Stäbchen wird in die Mitte des Tisches gelegt. Die Absprache (oder auch Programmierung) ist nun die, dass jeder Teilnehmer des seltsamen Mahls von seinem linken (oder rechten, es muss nur eindeutig sein) Nachbarn das vierte Stäbchen gereicht bekommt, sobald dieser etwas zu sich genommen hat. Wenn derjenige, der das Stäbchen bekommen hat, etwas zu sich genommen hat, gibt er es an den rechten Nachbarn weiter. So können alle mit der Zeit etwas zu sich nehmen und es wird sicher ein unterhaltsames Mahl.

Wie sieht es in der Praxis aus? Fangen wir ohne Beschränkung der Allgemeinheit mit Chinese Nummer 1 an. Er wartet darauf, dass er das Stäbchen von seinem linken Nachbarn (Nummer 3) bekommt. Dieser wartet darauf, es von seinem linken Nachbarn (Nummer 2) zu bekommen. Und dieser wiederum wartet darauf, es von seinem linken Nachbarn (Nummer 1) zu bekommen.

Da Chinesen höflich sind, wird keiner als erstes das 4. Stäbchen aufnehmen und dementsprechend nie das Ereignis eintreten, auf das alle - langsam sicher extrem hungrigen - Teilnehmer des geplatzten Mahls warten.

Um Sie zu beruhigen - die drei armen Kerle werden nicht verhungern. Sehr wahrscheinlich siegt irgendwann der Hunger über die Höflichkeit oder die Etikette (dann wird halt mit der Hand gegessen oder einer greift doch nach dem Stäbchen).

Bei Threads entsteht Hunger höchstens beim Anwender und das hilft in der Situation überhaupt nichts. Selbst oder gerade synchronisierte Threads dürfen auf keinen Fall so gebaut werden, dass sie auf ein Ereignis warten, das sie selbst blockieren. Man nennt eine solche Patt-Situation zwischen Threads einen Deadlock.

8.4.3 Synchronisierte Blöcke

Bei der Technik des Multithreadings muss es die Möglichkeit geben, genau wie bei Methoden ganze Blöcke, die auf die gleiche Information zugreifen wollen, zu synchronisieren.

Um Blöcke zu synchronisieren, kann man auch hier wieder das Schlüsselwort synchronized verwenden. Diesmal wird damit allerdings ein synchronisierter Block erstellt.

Ein skizziertes Java-Programm soll eine Verwendung des Schlüsselworts synchronized demonstrieren.

class Netzwerk { 
Zugriff oeffneSchnittstelle; 
... 
synchronized (oeffneSchnittstelle) { 
online(oeffneSchnittstelle.sende[1]); 
online(oeffneSchnittstelle.sende[2]); 
}   }

Genau wie synchronisierte Methoden verschließen synchronisierte Blöcke ein spezifisches Objekt, solange sie laufen. Damit werden auch verschiedene synchronisierte Blöcke nicht gleichzeitig abgearbeitet, solange ein anderer synchronisierter Prozess abläuft.

Es gibt jedoch in der Tat Unterschiede zwischen synchronisierten Methoden und synchronisierten Blöcken. Synchronisierte Methoden verschließen die derzeitige Klasse, während synchronisierte Blöcke nur die spezifizierte Methode während der Ausführung verschließen. Im obigen Beispiel wird die Instanz namens oeffneSchnittstelle verschlossen, nicht jedoch die Klasse Netzwerk.

8.4.4 Thread-sicherer Zugriff auf Datenstrukturen und Objekte

Das Multithreadingkonzept erzwingt die Möglichkeit eines Thread-sicheren Zugriffs auf Datenstrukturen und Objekte. Dieser ist in Java realisiert und trägt viel zur allgemeinen Stabilität eines Java-Systems bei. Dazu verwendet Java einen neuen und einzigartigen Ansatz, um Funktionen aufzurufen. Normalerweise rufen PC-Programme Funktionen über eine numerische Adresse auf. Da diese Adresse eine numerische Folge ist und diese Folge so programmiert werden kann, wie der Programmierer es sich wünscht, kann eine beliebige Zahl genommen werden, um dem Programm zu sagen, wie es eine Funktion ausführen soll. Damit wird eine Anonymität aufrechterhalten, mit der es unmöglich ist herauszufinden, welche Funktionen tatsächlich verwendet werden, wenn das Programm läuft. Java seinerseits verwendet Namen zum Aufruf von Methoden und Variablen. Auf Methoden und Variablen kann allein mit dem Namen zugegriffen werden. Zu bestimmen, welche Methoden verwendet werden, ist also einfach. Diese Verifizierung wird u.a. dazu verwendet, sicherzustellen, dass der Bytecode nicht beschädigt oder verändert wurde und dabei die Anforderungen von Java als Sprache erfüllt.

8.5 Zusammenfassung

Threads ermöglichen Java, Aufgaben in einem Programm sinnvoll aufzuteilen. Multithreading bedeutet im Wesentlichen, dass mehrere Aufgaben oder Prozesse quasi gleichzeitig ausgeführt werden können. Bei Java ist das Multithreadingkonzept voll integrierter Bestandteil der Philosophie. Der hochentwickelte Befehlssatz in Java, um Threads zu synchronisieren, ist in die Sprache integriert, macht diese stabil und einfach in der Anwendung. Java verwendet hochintelligente Synchronisationsanweisungen für den Umgang mit Multithreading. Die Schlüsselworte synchronized und threadsafe werden zum Markieren von Blöcken und Methoden benutzt, die eventuell vor gleichzeitiger Verwendung geschützt werden sollen.

Sie können Ihre Anwendungen und Klassen auf zwei Arten Multithreading-fähig machen. Einmal, indem Sie die Thread-Klasse erweitern oder indem Sie die Schnittstelle Runnable implementieren.

Applets nutzen meist die zweite Variante, um multithreading-fähig zu werden und verwenden die run()-Methode statt der start()-Methode zum Aufruf des eigentlichen Programmcodes.

Java stellt eine Vielzahl von Methoden und weitergehenden Konzepten zur Verfügung, die die Multithreading-Technik unterstüzen. Insbesondere können Threads in so genannte Thread-Gruppen eingeteilt werden, die im Wesentlichen eine Sicherheitsstrategie für Threads festlegen.

1

Das gilt auch für die später noch besprochene Implementation von Runnable.

2

Das ist auch der Grund, warum die Beendigung der main()-Methode bei noch aktiven weiteren Benutzer-Threads nicht mit dem Programmende identisch ist.


© Copyright Markt+Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH
Elektronische Fassung des Titels: Java 2 Kompendium, ISBN: 3-8272-6039-6 Kapitel: 8 Multithreading