Galileo Computing <openbook>
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net


Java 2 von Friedrich Esser
Designmuster und Zertifizierungswissen
Zum Katalog
gp Kapitel 9 Threads
  gp 9.1 Grundlegende Begriffe
  gp 9.2 Thread-Start
  gp 9.3 Thread-Zustände
  gp 9.4 Thread-Methoden
  gp 9.5 Race-Condition
  gp 9.6 Synchronisation und Deadlock
  gp 9.7 Vermeidungsstrategien zu Deadlocks
  gp 9.8 Guarded-Method: wait() und notify()
  gp 9.9 Weitere thread-sichere Maßnahmen
  gp 9.10 Thread-Mechanismen
  gp 9.11 Client-aktivierte asynchrone Methoden
  gp 9.12 Server-Aktivierung
  gp 9.13 Thread-Unterbrechung
  gp 9.14 Unbehandelte Ausnahmen in Threads
  gp 9.15 Zusammenfassung
  gp 9.16 Testfragen19

Kapitel 9 Threads

Die direkte Unterstützung von Threads in Java ist neu im
Vergleich zu C/C++, die hierfür keine direkte Sprachunterstützung vorsehen.
Der sinnvolle Einsatz von Threads macht Programme reaktiver, da mehrere Aktivitäten quasi gleichzeitig ausgeführt
werden können, und die Reaktionszeit gegenüber einem rein sequenziellen Ablauf zunimmt.
Die Darstellung des Themas »Threads« – beschränkt auf ein Kapitel – bedeutet die Vermittlung von Standardwissen zu grundlegenden Mechanismen und einige Regeln und Muster, vor allem zur Vermeidung von Problemen.

Threads:
nebenläufige leichtgewichtige Prozesse

Threads werden auch als nebenläufige leichtgewichtige Prozesse bezeichnet. Nebenläufig, weil sie quasi parallel nebeneinander her laufen, leichtgewichtig, weil sie sich mit anderen Threads dieselben Daten teilen.

Damit können Threads vom Betriebssystem schneller abwechselnd ausgeführt werden als Prozesse, d.h. parallel laufende Programme.

Jeder Thread ist ein einzelner sequenzieller Weg der Ausführung von Code. Dabei stehen die Threads an verschiedenen Stellen im gemeinsam genutzten Code und konkurrieren untereinander beim Zugriff auf Daten bzw. Objekte.

Diese Parallelität, der Einsatz von aktiven gegenüber passiven Objekten, ist eine Herausforderung, die zu neuen Regeln und Mustern führt.


Galileo Computing

9.1 Grundlegende Begriffe  downtop


Galileo Computing

9.1.1 Multi-Tasking, Multi-Threading  downtop

Von Betriebssystemen (OS) her kennt man zwei grundlegende Begriffe:

Multi-Tasking

gp  Unter Multi-Tasking versteht man die quasi-parallele Ausführung von Programmen unter einem OS (siehe Abb. 9.1).

Multi-Threading

gp  Unter Multi-Threading versteht man die quasi-parallele Ausführung von Code-Abschnitten auf gemeinsamen Daten und Objekten in nur einem Programm (siehe Abb. 9.2).

Bei Multi-Tasking kann man also mehrere (gleiche oder unterschiedliche) Programme separiert voneinander ausführen.

Threads arbeiten mit gemeinsamen Daten

Threads werden dagegen innerhalb eines Programms mit gemeinsamen Daten und Objekten ausgeführt, wobei dann Änderungen durch ein Thread Auswirkungen auf die anderen hat.

Prozess-
Ausführung


Abbildung
Abbildung 9.1   Multi-Tasking

Natürlich gibt es auch für die Prozess-Kommunikation einen vom Betriebssystem zur Verfügung gestellten Datenspeicher, der zum Datentransfer genutzt werden kann (siehe Abb. 9.1).


Abbildung
Abbildung 9.2   Multi-Threading

Lokale Variablen sind thread-eigen

Umgekehrt sind lokale Variablen von Methoden thread-eigen, wobei aber bei lokalen Referenzen zwischen der thread-eigenen Referenz-Variablen und dem referenzierten Objekt unterschieden werden muss.


Galileo Computing

9.1.2 Scheduling, Priorität und Preemption  downtop

Prozesse und Threads werden von einem Scheduler (Planer) im Betriebssystem verwaltet, d.h., alle aktiven Threads werden in eine Warteschlange eingeordnet und je nach Priorität für kurze Zeitintervalle ausgeführt.

Scheduling:
Ausführungsplan

gp  Mit Scheduling bezeichnet man den Ausführungsplan zum Umschalten der aktiven Threads. Scheduling beruht auf Preemption und/oder Time-Slicing.

Priorität – Preemption

gp  Jeder Thread hat eine Priorität. Bei Preemption (nach dem engl. Verb preempt, dt. zuvorkommen) werden immer die Threads mit der höchsten Priorität ausgeführt, die anderen müssen warten.

Time-Slicing

gp  Beim Time-Slicing kommt nach einem gewissen Zeitintervall – auch Zeitscheibe genannt – immer ein anderer Task/Thread zur Ausführung, ohne dass der laufende darauf Einfluss hat.

Die Art des Schedulings kann nicht in Java spezifiziert werden. Java-Prioritäten werden auf Betriebssystems-Prioritäten abgebildet, wobei diese Abbildung nicht 1:1 und keineswegs eindeutig ist. Nach welchen Heuristiken der Scheduler Threads dann noch Zeitscheiben zuordnet, ist nicht vorhersehbar.

Nicht deterministisches Scheduling

Der Ausführungsplan muss also als nicht deterministisch angesehen werden. Wann und wie lange welcher Thread zur Ausführung kommt, hängt vom Scheduler und dem jeweiligen Zustand des Systems ab.


Abbildung
Abbildung 9.3   Ausführung der Methode obj.service() in zwei Threads

Eine Methode, z.B. service(), kann dabei durchaus von zwei oder mehreren Threads für dasselbe Objekt obj gleichzeitig ausgeführt werden (siehe Abb. 9.3).

Dies macht Laufzeittests auf Fehlerfreiheit mühsam, da sich aufgrund der Testläufe nicht schlüssig ermitteln lässt, wie die nächste Ausführung beim Kunden aussieht.


Galileo Computing

9.1.3 Synchronisation  downtop

Parallel laufende Threads sind prinzipiell in ihrem Kontrollfluss bzw. in ihren Aktivitäten unabhängig. Dies bedeutet, dass sich alle Threads erst einmal asynchron verhalten, zu jedem Zeitpunkt jede Instanz- oder Klassen-Methode ausführen können.

Synchrones vs. asynchrones
Verhalten

gp  Jede Art des Wartens aufeinander nennt man Synchronisation, das Gegenteil ist asynchrones Verhalten.

Wird eine Methode aufgerufen, ohne auf ihr Operationsende zu warten, ist dies ein asynchroner Aufruf, ansonsten ein synchroner. Wartet ein Thread auf die Beendigung eines anderen, nennt man sie sychronisiert.

Locking: Sperren von Objekten bzw. Klassen

Java synchronisiert grundsätzlich mit Hilfe von Locks (Sperren) auf Objekte oder auf Klassen (einem Lock auf Class-Objekte). Jedes Objekt bzw. jede Klasse (Class-Objekt) hat genau ein Lock.

synchronized

Wird irgendeine Klassen- oder Instanz-Methode mit dem Schlüsselwort synchronized markiert, so muss sich ein Thread für die Ausführung dieser Methoden erst einmal das zugehörige Objekt- bzw. Klassen-Lock holen.


Abbildung
Abbildung 9.4   Synchronisierte Ausführung von obj.service()

Besitz des Locks: exklusive Ausführungsrechte

Während ein Thread (Thread 1 in Abb. 9.4) ein Lock besitzt, kann kein anderer dieses erwerben und hat somit keinerlei Ausführungsrechte für alle synchronisierten Methoden dieses Objekts bzw. dieser Klasse.

Der Thread, der das Lock für eine synchronisierte Methode erworben hat, kann nicht nur die eine, sondern alle synchronisierten Methoden des Objekts bzw. der Klasse exklusiv ausführen.

Nachdem ein Thread die letzte synchronisierte Methode eines Objekts bzw. einer Klasse beendet hat, wird das entsprechende Lock wieder freigegeben und kann von einem anderen Thread erworben werden (Thread 2 in Abb. 9.4).

Deadlock

Deadlock: Gegenseitige Blockade von Threads

Mit Einführung der Synchronisation besteht die Gefahr, dass zwei Threads gegenseitig aufeinander warten oder – schlimmer – alle aufeinander warten, d.h., das gesamte System »friert ein«.

Diese Situation ist aus dem Straßenverkehr bekannt, wo an einer Kreuzung vier Fahrzeuge aus den verschiedenen Richtungen gleichzeitig ankommen und aufgrund der Rechts-vor-Links-Regel alle aufeinander warten müssen. Dieser Deadlock wird dann durch einen mutigen Fahrer bzw. einen Unfall beseitigt.


Galileo Computing

9.2 Thread-Start  downtop

Zwei Alternativen zum Starten eines Threads

Startpunkt eines Threads ist immer ein Objekt der Klasse Thread, wobei es allerdings zwei Alternativen gibt (Abb. 9.5).


Abbildung
Abbildung 9.5   Zwei Alternativen zum Starten eines Threads

zu A1: Dies ist eher die Ausnahme. Will man wirklich nur einen Thread erschaffen, dann ist diese Alternative ok und auch einfacher (Abb. 9.6).

zu A2: Dies ist der Normalfall. Man will Objekte konkreter Klassen wie z.B. einen Druck-Manager, einen Video-Player, eine Kalkulation oder Transaktion erschaffen, welche ihren Service asynchron ausführen sollen.

Service als Thread oder asynchroner Dienst

Dies bedeutet aber, dass man eine von der Thread-Klasse unabhängige Klasse bzw. Klassen-Hierarchie entwerfen muss, mit der zusätzlichen Eigenschaft, asynchron ausgeführt werden zu können (Abb. 9.6).


Abbildung
Abbildung 9.6   Service-Klasse als Sub-Thread vs.»mit asynchronem Dienst«
Galileo Computing

9.2.1 Methoden run() und start()  downtop

main() und run():
Start- und Endpunkt der Ausführung

Die Methode run() ist äquivalent zur Methode main() einer Applikation. Die Methode main() bildet den Start- bzw. Endpunkt des Hauptthread, run() den Start- und Endpunkt eines zusätzlichen Threads. Thread wie Applikation sind mit dem Ende von run() bzw. main() beendet.

Ist run() beendet, sagt man auch, dass der Thread den Endzustand tot (dead) erreicht hat, kurz tot ist.

Icon

Es sind folgende vier Regeln zur Ausführung wichtig:

Thread-
Ausführung

1. Ein Thread muss immer mittels start() gestartet werden, der direkte Aufruf von run() führt zwar die Methode aus, aber nicht als neuen, eigenständigen Thread.
2. Ein Thread, der beendet (tot) ist, kann nicht wieder neu gestartet werden, weder mittels start() noch mittels run().

3. Um ein Thread erneut zu starten, muss eine neue Thread-Instanz erschaffen und wieder mittels start() gestartet werden.
4. Das thread-fähige Objekt existiert unabhängig von dem (System-) Thread, den es auslöst (siehe Abb. 9.6). Die Methoden des Objekts können unabhängig davon, in welchem Zustand der Thread ist, immer von allen anderen Threads ausgeführt werden.1

Thread-Start-Muster

Icon

Nachfolgend zwei einfache Muster, um einen Thread zu erzeugen:

Muster für einen Thread-Start

// 1. Muster
class SubThread extends Thread { public void run() { ... } // siehe 1. Regel }
// 2. Muster
class ClassWithThread implements Runnable { public void run() { ... } // siehe 1. Regel }
// Testklasse zur Anlage und Start der Threads
public class TestThread { public static void main(String[] args) { Thread st1= new SubThread(); Thread st2 = new Thread(new ClassWithThread());
    st1.start();     // Methode start() ruft run() 
auf
    st2.start();     // run() läuft asynchron zu main()
  }
}

Dieses Testbeispiel zeigt noch ein interessantes Detail:

Ende einer
Java-App

gp  Eine Java-App läuft so lange, bis der letzte Nicht-Dämon-Thread beendet ist.

Im Testbeispiel ist zwar main(), d.h. der Hauptthread, sofort nach Aufruf der beiden Methoden start() beendet, aber die Java-App ist erst beendet, wenn die beiden anderen Threads beendet sind.

Würde man in main() die Methode st1.run() anstatt st1.start() ausführen, wäre dies eine synchrone Ausführung im Hauptthread (siehe 4. Regel).

Bei synchroner Ausführung muss zuerst st1.run() beendet werden, bevor die nächste Anweisung st2.start() ausgeführt wird.


Galileo Computing

9.3 Thread-Zustände  downtop

Zustandswechsel einer Thread

Um die Methoden, die auf Threads operieren, zu verstehen und sinnvoll zu benutzen, muss man die möglichen Zustände kennen, in denen sich ein Thread befinden kann (Abb. 9.7).


Abbildung
Abbildung 9.7   Diagramm der Thread-Zustände

Zustand: aktiv bzw. tot


Galileo Computing

9.3.1 Zustand: aktiv bzw. tot  downtop

Nach dem Start des Threads wird der Thread aktiv und wechselt aufgrund des Scheduling zwischen lauffähig und code-ausführend (»läuft«).

Ist run() beendet oder hat man die unsichere deprecated Methode stop() aufgerufen, so ist der Thread beendet, kurz tot.

Suspendierende Zustände

Daneben gibt es noch vier Zustände, in die ein Thread absichtlich oder unbeabsichtigt geraten kann, und die man auch generell mit dem Begriff suspendierend bezeichnen kann. Es ist aber wichtig, sie aufgrund ihres Typs zu unterscheiden.


Galileo Computing

9.3.2 Zustand: schlafend  downtop

Zustand:
schlafend

Der Zustand »schlafend« wird nur durch die statische Thread-Methode sleep(long ms) ausgelöst und lässt den ausführenden Thread für die angegebene Zeit in Millisekunden ruhen. Danach geht er wieder in den aktiven Zustand über.


Galileo Computing

9.3.3 Zustand: blockiert vs. nicht blockiert bei I/O  downtop

Zustand: blockiert vs. nicht blockiert bei Input/Output

Ist ein Thread mit Daten-Ein-/Ausgabe (I/O) beschäftigt, muss er eventuell aufgrund nicht bereiter I/O-Geräte warten. Dies ist eigentlich sogar der Normalfall, da in der Regel Peripherie-Geräte wesentlich langsamer in der Ausführung sind als die CPU.

Ein Thread, der auf I/O wartet, wird von der JVM suspendiert bzw. blockiert, damit andere aktive Threads ausgeführt werden können. Der Thread wird erst wieder aktiv, wenn die Daten bereitstehen oder geschrieben wurden. Diese Art der Ein-/Ausgabe ist also synchron.

»Friert« ein I/O-Gerät ein, bedeutet dies, dass der zugehörige Thread nicht mehr läuft, was auch eine Art von Tod darstellt.

nonblocking I/O

Das Gegenteil ist »nicht blockierend bei I/O« (nonblocking I/O). Hier wartet der Thread nicht. Liegen Daten von der Eingabe bereit, liest er sie oder er kehrt sofort wieder zurück. Kann der Thread bei der Ausgabe Daten nicht schreiben, kehrt er ebenfalls sofort zurück.

Keine asynchrone I/O

gp  Java unterstützt – mit wenigen Ausnahmen – nur synchrone I/O, d.h., es gibt keine direkte Unterstützung für eine nicht blockierende I/O.

Galileo Computing

9.3.4 Zustand: Warten auf Lock  downtop

Zustand: Warten auf Lock

Der Mechanismus der Synchronisation wurde bereits in 9.1.3 beschrieben. An dem Zustands-Diagramm in 9.7 erkennt man schon die Gefahr des Deadlocks. Wartet ein Thread auf den Eintritt in einen synchronisierten Code-Bereich und bekommt das Lock nicht, ist er so gut wie tot.


Galileo Computing

9.3.5 Monitor  downtop

Bei Threads, die auf denselben Objekten operieren, gibt es zwei Möglichkeiten der Kontrolle.

Entweder prüfen die Threads, ob der Zustand der Objekte passend ist, oder umgekehrt, die Objekte kontrollieren aufgrund ihrer Zustände die Threads. Bei Java hat man sich für die zweite Möglichkeit entschieden.

Monitor: Die Objekte kontrollieren die Threads

gp  Ein Objekt, das die auf ihm operierenden Threads suspendieren und wieder aktivieren kann, heisst Monitor.
gp  Jedes Objekt, das synchronisierten Code enthält, ist ein Monitor.

Galileo Computing

9.3.6 Methoden wait(), notify() bzw. notifyAll()  downtop

Synchronisation mittels wait() und notify()

Zur Kontrolle der Threads hat die Klasse Object und somit jedes Objekt die Methoden wait() und notify() bzw. notifyAll().

Innerhalb einer synchronisierten Methode versetzt wait() einen Thread in den Zustand »wartend«.

Ist ein passender Zustand erreicht, kann das Objekt eine oder alle wartenden Threads mittels notify() bzw. notifyAll() in der gleichen oder einer anderen synchronisierten Methode wieder aktivieren. Dieser Aufruf kann natürlich nur aus einem anderen aktiven Thread heraus erfolgen (siehe Abb. 9.8).


Abbildung
Abbildung 9.8   Objekt als Monitor mit Methoden wait() und notify()
Galileo Computing

9.3.7 Zustand: wartend  downtop

Zustand: wartend

Jede Instanz hat einen eigenen Zustand »wartend«, in den alle Threads, die wait() auf diese Instanz ausführen, überführt werden (siehe Abb. 9.8, Thread 1).

Icon
notify(): Verlassen des Wartezustands

Kurz zu den Regeln, verbunden mit notify():

gp  Wird in derselben Instanz ein notify() von einem noch aktiven Thread ausgeführt und mehr als ein Thread wartet, wird ein zufälliger Thread in den aktiven Zustand überführt.

Kein FIFO-Prinzip

gp  Es gibt bei notify() keine Reihenfolge à la FIFO (First-In First-Out ).
gp  Die Methode notifyAll() aktiviert alle zur Instanz wartenden Threads.

Wer zuerst wartet, wird also bei notify() bzw. notifyAll() nicht unbedingt zuerst wieder aktiviert. Die Auswahl eines Threads ist willkürlich (in Abb. 9.8 gibt es allerdings nur einen wartenden Thread 1).

Für statische Methoden läuft der oben beschriebene Mechanismus analog, nur eben auf Klassen-Ebene, d.h., der Monitor ist das Objekt Class.


Galileo Computing

9.3.8 Unterbrechen der Zustände  downtop

Wie in Abb. 9.7 zu sehen, können die suspendierenden Zustände »schlafend« oder »wartend« unterbrochen werden. Dazu muss zu diesem Thread die Instanz-Methode interrupt() von einem anderen laufenden Thread aufgerufen werden.

interrupt(): Unterbrechung der Zustände wartend und schlafend

Die Methode interrupt() bewirkt bei einem

gp  suspendierten Thread einen Übergang nach »aktiv« und die Auslösung einer InterruptedException, die – da keine Runtime-Exception – abgefangen werden muss.

Interrupt-Flag

gp  aktiven Thread, dass ein Interrupt-Flag gesetzt wird, das entweder während der Ausführung abgefragt werden kann oder beim nächsten suspendierenden Zustand dann die Ausnahme auslösen sollte (Näheres siehe 9.13).

InterruptedIOException, im Prinzip ja, aber ...

Korrekterweise müsste in Abb. 9.7 auch beim Verlassen des Zustands »blockiert« ein »unterbrochen« eingetragen werden, denn auf eine Unterbrechung sollten I/O-Methoden mit einer InterruptedIOException reagieren.

Das ist aber bei den meisten IO-Methoden nicht implementiert, selbst System.in von Sun reagiert darauf nicht. Deshalb wurde dieser Guard weggelassen. Hier helfen nur Abbrüche durch Time-outs.


Galileo Computing

9.4 Thread-Methoden  downtop

Die Klasse Thread enthält als zentraler Repräsentant weitere wichtige Methoden, die kurz vorgestellt werden sollen.


Galileo Computing

9.4.1 Konstruktoren  downtop

Es gibt drei bzw. vier Konstuktoren für die beiden Alternativen der Thread-Erzeugung (siehe 9.2). Für die zweite Alternative muss bei der Anlage der Thread-Instanz ein target-Objekt übergeben werden, das run() enthält. Jedem Thread kann optional ein Name gegeben und eine Thread-Gruppe zugeordnet werden.

Thread-Gruppen

gp  Thread-Gruppen sind dazu da, Threads nach ihrer Funktionalität zu gruppieren, um dann alle Threads einer Gruppe gleichzeitig manipulieren zu können.

Nachfolgend die sieben Konstruktoren (target teilweise optional):

Thread-
Konstruktoren

 public Thread( [Runnable target] );
 public Thread( [Runnable target,] String name);
 public Thread(ThreadGroup group, Runnable target);
 public Thread(ThreadGroup group, [Runnable target,] String name);

Galileo Computing

9.4.2 Statische Methoden  downtop

Statische Methoden der Klasse Thread

Es folgen einige wichtige statische Methoden:

gp  static int activeCount(): Ermittelt die Anzahl der zur Zeit aktiven Threads der aktuellen Thread-Gruppe.
gp  static Thread currentThread(): Ermittelt den zur Zeit laufenden (code-ausführenden) Thread.
gp  static boolean interrupted(): Ein Thread kann einen anderen unterbrechen (siehe 9.3.8). Bei einem aktiven Thread wird das »unterbrochen«-Flag mit interrupted() getestet und anschließend wieder auf false gesetzt (siehe Alternative isInterrupted() in 9.4.4).
gp  static void sleep(long ms) throws InterruptedException: Der laufende Thread wird für die angegebene Zeit in Millisekunden in den Zustand »schlafend« versetzt.
gp  static void yield(): Überlässt einem anderen lauffähigen Thread die Möglichkeit der Ausführung (ist abhängig vom Scheduler!).

Galileo Computing

9.4.3 Prioritäten  downtop

Prioritäten: MIN_PRIORITY .. MAX_PRIORITY

Jeder Thread hat eine Priorität (siehe 9.1.2). Java kennt Prioritäten zwischen static final int MIN_PRIORITY (=1) und MAX_PRIORITY (=10).

Die vorgegebene Default-Priorität ist NORM_PRIORITY (=5), womit z.B. der Hauptthread, gestartet durch main(), beginnt.

gp  Java-Prioritäten können nur im Idealfall 1:1 auf Betriebssystems-Prioritäten abgebildet werden.

Scheduler und
Prioritäten

In welchem Maße der Scheduler Threads mit höherer Priorität bevorzugt, ist nicht festgelegt. Es liegt zwischen den Extrema »nur Threads mit der höchsten Priorität sind laufberechtigt« und »alle Threads, egal welcher Priorität, sind gleich laufberechtigt«.

Die Priorität eines Threads kann zur Laufzeit gelesen und gesetzt werden (siehe 9.4.4).

Icon

Für die Prioriät beim Start gilt folgende Regel:

Default-Priorität eines Threads

gp  Ein neuer Thread übernimmt bei der Anlage die Prioriät des Threads, aus dem er erschaffen wurde.

Galileo Computing

9.4.4 Instanz-Methoden  downtop

Instanz-Methoden der Klasse Thread

Von den insgesamt 20 nicht deprecated Instanz-Methoden werden hier nur die wichtigsten, noch nicht besprochenen vorgestellt.

gp  public final int getPriority(),
public final void
setPriority(int newPriority): Liest oder setzt die Priorität dieses Threads.
gp  public final ThreadGroup getThreadGroup(): Liest die Thread-Gruppe, zu der dieser Thread gehört.
gp  public void interrupt(): Löst für diesen Thread eine Unterbrechung aus (Details siehe 9.3.8).
gp  public boolean isInterrupted(): Testet für diesen Thread, ob eine Unterbrechung vorliegt, ohne das »unterbrochen«-Flag zu verändern (siehe Alternative interrupted()).
gp  public final native boolean isAlive(): Liefert true, wenn dieser Thread gestartet wurde, aber noch nicht tot ist, ansonsten false.
gp  public final void join( [long ms] ) throws InterruptedException: Blockiert den Thread, der join() ausführt, so lange bis der Thread, verbunden mit der Instanz, stirbt bzw. tot ist. Bei einer Zeitangabe ist der Thread maximal ms Millisekunden blockiert (siehe auch Abb. 9.7).

Dämon-Threads:
ohne eigene Existenzberechtigung

gp  public final void setDaemon(boolean on): Setzt den Thread als Dämon-Thread (im Sinne von Hintergrunds-/Dienst-Thread). Diese Anweisung hat unbedingt vor dem Thread-Start zu erfolgen.
Dämon-Threads zählen bei der Entscheidung nicht mit, ob das Programm von der JVM weiter ausgeführt werden muss. Gibt es nur noch Dämon-Threads, ist die Ausführung beendet.
Galileo Computing

9.4.5 Diverse Methoden im Beispiel  downtop

Nach dem Überblick über Konzepte und Instrumente soll ein erstes kleines Beispiel den Einsatz und die Wirkung einiger der genannten Methoden demonstrieren.

Zuerst werden zwei Threads nach den beiden in 9.2 beschriebenen Alternativen angelegt und dann diverse statische und nicht statische Methoden der Thread-Klasse getestet:

// Hilfs-Klasse 
class TestRun { 
 // der aktuelle Thread maximal n Mal für
// sleepTime Millisec. schlafen lassen
static void run(int sleepTime, int n ) { try { while (n-->0) { System.out.println(Thread.currentThread().getName()); Thread.sleep(sleepTime); } } catch (InterruptedException e) { System.out.println("Interrupted: "+Thread.currentThread()); } } }

Subklasse von Thread: Overriding run()

class SubThread extends Thread {
   public void run() { 
     TestRun.run(1000,3); // dreimal 1 sec schlafen
  } 
}

Implementation von Runnable

class ClassWithThread implements Runnable {
  public void run() {
   TestRun.run(1000,3); // dreimal 1 sec schlafen
  }
}

Test diverser Thread-Methoden

public class Test {
  public static void main(String[] args) {
    Thread st0= new SubThread();
    Thread st1= new Thread(new ClassWithThread());
    // st1.setDaemon(true);                        
             ¨
    System.out.println(Thread.currentThread().getThreadGroup());
    System.out.println(Thread.currentThread().getPriority());
    st0.start(); st1.start(); 
    st0.interrupt();
  }
}
Tabelle 9.1   Mögliche Konsol-Ausgabe zur Applikation »Test«
 java.lang.ThreadGroup[name=main,maxpri=10]
 5
 Thread-1
 Thread-0
 Interrupted: Thread[Thread-0,5,main] 
 Thread-1
 Thread-1

Default-Thread-Gruppe

Erklärung: Das erste println() gibt mit Hilfe von toString() die Standard-Thread-Gruppe aus, zu der der Hauptthread gehört. Threads ohne Namen werden automatisch mit Null beginnend numeriert. Danach folgt die Prioritäts-Angabe des Hauptthreads.

Obwohl Thread-0 vor Thread-1 gestartet wurde, entscheidet allein der Scheduler über die CPU-Zuteilung, d.h., wer sich auf der Konsole zuerst meldet, ist Sache des Schedulers.

Thread-0 wird im Schlaf-Zustand unterbrochen, was eine InterruptedException auslöst (siehe Konsol-Meldung). Thread-1 durchläuft dagegen dreimal die Schleife, was zu den letzten beiden Ausgaben führt.

Nur noch
Dämon-Threads: Programmende

zu ¨: Wird die auskommentierte Anweisung ausgeführt, ist Thread-1 ein Dämon. Das Programm ist somit beendet, wenn Hauptthread sowie Thread-0 terminieren, d.h., Thread-1 wird sich nur einmal melden.


Galileo Computing

9.5 Race-Condition  downtop

Wenn Threads parallel auf dieselben Objekte zugreifen können, wird die Frage interessant, in welcher Reihenfolge Lese- und Schreib-Operationen auf den Objekten ablaufen.


Galileo Computing

9.5.1 Atomare vs. nicht atomare Operationen  downtop

Atomare Operation: nicht unterbrechbar

Unter einer atomaren Operation versteht man eine Operation, die entweder als Ganzes oder überhaupt nicht durchgeführt wird. Beginnt die Operation, kann sie nicht mehr unterbrochen werden, sie ist unteilbar. Das Gegenteil ist nicht atomar.

Betrachten wir ein einfaches Objekt Int:

Primitive Typen == atomare Operationen ?

class Int {
  private int i;
  boolean test(int j) { 
    i= j; 
    return i==j;
} }

In einer single-threaded Applikation ist das Ergebnis der Methode test() trivial, nämlich true.

Bei Multi-Threading lautet die Frage, ob test() atomar ist. Die Methode ist dann atomar, wenn Setzen auf j und Vergleich mit j nicht unterbrechbar sind, also ganz oder gar nicht ablaufen.

Nachfolgend ein einfacher Test von Int mit Konsol-Ausgabe:

Testprogramm
zu atomaren Operationen

class AtomicTest implements Runnable 
{
  Int iobj;
  AtomicTest(Int iobj) { 
    this.iobj= iobj; 
  }
  public void run() {
    int i= 0;
    // maximal 100000 mal auf true testen
    while (iobj.test(i) && ++i <100000);
    System.out.println(Thread.currentThread().getName()+": "+i);
  }
}

In der Klasse Test wird die Methode test() von genau einer Int-Instanz parallel von vier Threads ausgeführt:

Ein Objekt, vier schreibende Threads

public class Test {
  public static void main(String[] args) {
    Int iobj= new Int();                  // nur ein Objekt
    Thread[] st= new Thread[4];

Lesen und Schreiben des
primitiven Typs int ist nicht atomar

    for (int i=0; i<st.length;i++) {
      st[i]= new Thread(new AtomicTest(iobj)); 
   // st[i].setPriority(Thread.MAX_PRIORITY-2*i);             ¨
      st[i].start();
    }                                            
  }
}
Tabelle 9.2   Drei von vielen möglichen Konsol-Ausgaben der Klasse Test
 Thread-2: 100000
 Thread-0: 100000
 Thread-1: 100000
 Thread-3: 100000
 Thread-0: 15
 Thread-1: 100000
 Thread-2: 100000
 Thread-3: 100000
 Thread-1: 0
 Thread-3: 0
 Thread-0: 7
 Thread-2: 100000

Erklärung: Die Methode test() in der Klasse Int ist nicht atomar, vor dem Vergleich von i mit j kann ein anderer Thread bereits den Wert von i geändert haben, was allerdings recht selten vorkommt.

Prioritäten

Mit unterschiedlichen Prioritäten kann man versuchen, Einfluss auf den CPU-Zuteilungs-Algorithmus des Schedulers zu nehmen.

Wird die auskommentierte Anweisung ¨ in Test ausgeführt, so erhalten die Threads abnehmende Prioritäten.

Bei preemptive Scheduling sollte sich dann Thread-0 zuerst melden, danach Thread-1, Thread-2 und Thread-3. Dies ist allerdings wieder vom Scheduler abhängig.


Galileo Computing

9.5.2 Reentrant, Race-Condition und thread-sicher  downtop

Im Zusammenhang mit Thread-Problemen sind folgende Begriffe wichtig:

Reentrant Code

gp  Ein Code-Abschnitt (Methode oder Block) heißt reentrant, wenn er von mehreren Threads gleichzeitig ausgeführt wird.

Im letzten Beispiel war die Methode test() reentrant. Ein Thread führt i=j; aus, während ein anderer den Vergleich i==j; durchführt.

Race-Condition: Thread-Wettrennen

gp  Unter Race-Condition versteht man Fehler bzw. Probleme, die durch reentrant Code entstehen, d.h., Ergebnisse werden von der zufälligen Reihenfolge parallel ablaufender Operationen abhängig.

Icon

In Verbindung mit Reentrance gilt folgende Regel:

Atomare
Operationen

gp  Nur Lese- oder Schreib-Operationen auf Variablen bis vier Byte Länge sind atomar. Alle anderen Operationen sind nicht atomar.

Icon
Vermeidlich atomare
Operationen

Somit sind die meisten einzelnen Anweisungen nicht atomar und können ergo zu Race-Conditions führen. Dies ist sicherlich nicht sehr glücklich. Man betrachte die beiden »unverdächtigen« Anweisungen zu einer int i bzw. long l:

     i++;      // muss gelesen und geschrieben werden
     return l; // 8 Byte benötigen zwei Lese-Operationen

Dies sind zwei überraschende Beispiele für nicht atomare Anweisungen.

Thread-sicher:
»no problems« bei Multi-Threading

gp  Code bzw. Methoden heißen thread-sicher (thread-safe), wenn es zu keinen Problemen in einer Multi-Threading-Umgebung kommt.

Können Race-Conditions auftreten, ist der Code nicht thread-sicher. Leider ist dies nicht hinreichend, will sagen, nicht das einzige Problem.

Thread-Sicherheit erfordert in einer komplexen Ablaufumgebung umfangreiche Maßnahmen, von denen im Weiteren nur wenige besprochen werden können.


Galileo Computing

9.6 Synchronisation und Deadlock  downtop

Guarded
C
oncurrency: Single-Thread-Ausführung durch Synchronisation

Mit Hilfe des Schlüsselworts synchronized werden Methoden oder Blocks vor Reentrance geschützt (guarded):

gp  Der mit synchronized geschützte Code kann nicht gleichzeitig von mehreren Threads ausgeführt werden.

Logisch atomare Operation

Werden in einem synchronisierten Code-Block keine suspendierenden Anweisungen ausgeführt und gibt es nur Operationen auf primitiven Datentypen, ist der Code-Block sogar logisch atomar, d.h., zuerst müssen alle Anweisungen von einem Thread durchlaufen werden, bevor ein anderer denselben Code-Block ausführt.

Die Methode test() der Klasse Int in 9.5.1 kann also ganz einfach atomar und in diesem Fall dann auch thread-sicher gemacht werden:

Synchronisierte primitive
Operationen: thread-sicher und logisch atomar

class Int {
  private int i;
  synchronized boolean test(int j) { // guarded concurrency
    i=j; 
    return i==j;
  }
}

Bevor einer der vier Threads die Methode test() in dem Beispiel in 9.5.1 ausführen darf, muss er vom Monitor – der Instanz iobj der Klasse Int – das Lock erhalten. Hat ein Thread das Lock, gehen die anderen bei Aufruf von test() in den Zustand »warten auf Lock« über (siehe Abb. 9.7).


Galileo Computing

9.6.1 Methoden- und Block-Synchronisation  downtop

Die Synchronisierung kann auf

Synchronisation einer Methode

gp  Methoden-Ebene erfolgen:
synchronized [Modifiers] ResultType method (pList) ...
Dann muss der ausführende Thread bei einer Instanz-Methode zuerst das Lock des zugehörigen Objekts this, bei einer statischen Methode das Lock des Class-Objekts erwerben.

Synchronisation eines Blocks

gp  Block-Ebene erfolgen:
synchronized(ReferenzExpression) { synchronizedBlock }
Dann muss der ausführende Thread zuerst das Lock des Objekts erwerben, auf das ReferenzExpression zeigt.

Icon

Es gelten folgende drei Regeln:

Beschränkungen für synchronized

1. Konstruktoren können nicht synchronized werden.
2. In Interfaces ist synchronized nicht erlaubt.
3. Methoden können synchronized oder nicht überschrieben werden.

Thread-sichere Konstruktoren

Konstruktoren:
thread-sicher?

Konstruktoren brauchen nur synchronisiert werden, sollten sie während der Initialisierung eine Referenz des unfertigen Objekts this an andere Objekte herausreichen. In diesem Fall muss dann eben Block-Sychronisation verwendet werden.

Alternative Synchronisations-Mechanismen

Block-Synchronisation ist feiner

gp  Die Sychronisation auf Block-Ebene ist feiner und kann die der Methoden emulieren (siehe nachfolgendes Beispiel).

Beispiel

Icon

In der Klasse Sync werden verschiedene äquivalente Synchronisations-Mechanismen demonstriert.

Synchronisations-Muster

Die statischen Methoden sf1(), sfV1() und sfV2() sowie die Instanz-Methoden f1() und f2() sind äquivalent.

class Sync { 

Synchronisation von statischen Methoden

  // Statisch: Methoden- vs. Block-Synchronisation
  public synchronized static void sf1() 
{  
    //..  
  }
  public static void sfV1() {
    try  {
      // Einsatz von Reflexion
synchronized(Class.forName("Sync")) { //.. } } catch (ClassNotFoundException e) { } } public static void sfV2() { synchronized(new Sync().getClass()) { //.. } }

Synchronisation von Instanz-Methoden

  // Instanz: Methoden- vs. Block-Synchronisation
  public synchronized void f1() {  

    //..
  }
  public void f2() {
    synchronized(this) {  
      //..
    }
  }
}

Vorteile der Methoden-Synchronisation

Vorteile der Methoden-
Synchronisation

gp  Methoden-Synchronisation ist sofort im Methoden-Kopf zu erkennen, Block-Synchronisation dagegen ohne Zugriff auf den Code nicht.

Sofern als Lock andere Objekte als die eigene Instanz oder Klasse verwendet werden, sind die Auswirkungen der Synchronisation schwer abzuschätzen. Wird dies vom Anwender nicht durchschaut, kann es schnell zu Deadlocks kommen (siehe 9.6.3).


Galileo Computing

9.6.2 Voll- bzw. teilsynchronisierte Objekte  downtop

Ein Thread, der den Lock besitzt, kann als einziger beliebige synchronisierte Methoden bzw. Blöcke desselben Objekts ausführen.

Erst wenn ein Thread die letzte synchronisierte Methode des Objekts verlassen hat, kann ein anderer Thread den Lock erlangen.

Voll- vs. Teil-
Synchronisation

Sind – bis auf die Konstruktoren – alle Methoden eines Objekts synchronisiert und alle Felder private erklärt, so heißt eine Objekt vollsynchronisiert, ansonsten teilsynchronisiert.

Icon

Ist ein Objekt

Thread-Eintritt in Objekten

gp  vollsynchronisiert, so kann nur ein Thread zu einem Zeitpunkt auf dem Objekt operieren.
gp  teilsynchronisiert, so können neben dem Thread, der das Lock besitzt, beliebig viele andere Threads unsynchronisierte Methoden parallel im Objekt ausführen.

Galileo Computing

9.6.3 Deadlock durch Synchronisation  downtop

Deadlock-
Situation

Bei einem Deadlock sind zumindest zwei Threads im Zustand »warten auf Lock«, das jeweils einen anderen Thread im selben Zustand hält. Diese Threads blockieren sich gegenseitig und können nicht mehr aktiv werden.

Gibt es keinen aktiven Thread mehr, friert eine Java-App ein.

Beispiel

Die nachfolgende Klasse Int ist teilsynchronisiert. Ihre sychronisierte set()-Methode setzt ein internes int-Feld mit Hilfe einer ebenfalls synchronisierten get()-Methode.

Eine weitere unsychronisierte set()-Methode setzt das int-Feld direkt.

Icon
Deadlock- Situation: zwei Threads, zwei teil-synchronisierte Objekte

class Int {
  private int i;
  void set(int i)                 { this.i=i; }
  synchronized int get()          { return i; }
  synchronized void set(Int iobj) { i= iobj.get(); }
}

Threads der Klasse TestInt können wahlweise ohne Lock die unsynchronisierte set()-Methode der Klasse Int oder mit Lock die synchronisierte set()-Methode in einer for-Schleife testen.

class TestInt extends Thread {
  Int iobj1, iobj2;
  boolean syncset;
  TestInt (Int i1, Int i2, boolean syncset) {
    iobj1= i1; iobj2= i2;
    this.syncset= syncset;
  }
  public void run() {
    for (int i=0; i<20;i++)
      if (syncset)
        iobj1.set(iobj2);
      else {
        iobj1.set(i); iobj2.set(i);
      }
    System.out.println(getName()+"-Ende");
  }
}

Im Testprogramm werden zwei Int-Objekte und drei Threads angelegt. Thread-0 ruft die unsynchronisierte set()-Methode auf, Thread-1 und Thread-2 die synchronisierte, wobei allerdings die Int-Objekte vertauscht werden:

public class Test {
  public static void main(String[] args) {
    Int iobj1= new Int(); Int iobj2= new Int();

Threads konkurrieren um Locks

    new TestInt(iobj1,iobj2,false).start();   // 
ohne Synch
    new TestInt(iobj1,iobj2,true).start(); 
   //  mit Synch
    new TestInt(iobj2,iobj1,true).start(); 
   //  mit Synch
  }
}
Tabelle 9.3   Drei mögliche Konsol-Ausgaben der Applikation Test
 Thread-0-Ende
 Thread-1-Ende
 Thread-2-Ende
 Thread-0-Ende
 Thread-2-Ende
 Thread-1-Ende
 Thread-0-Ende

Erklärung: Thread-0 beendet immer erfolgreich seine Ausführung. Die letzte Ausgabe zeigt eine typischen Deadlock-Situation:

Sequenz-Diagramm zur Deadlock-
Situation


Abbildung
Abbildung 9.9   Deadlock von Thread-1 und Thread-2

Deadlock: Thread-1 sperrt Objekt iobj1 und ist im Zustand »Warten auf Lock iobj2« und Thread-2 sperrt Objekt iobj2 und ist im Zustand »Warten auf Lock iobj1«. Das Programm terminiert nicht (siehe 9.10).

Fazit:

Synchronisation: keine Races, aber Deadlocks

gp  Synchronisation beseitigt das Problem Race-Conditions und führt dafür zu Deadlock-Problemen.

Deadlock-sicher nur per Design

gp  Race-Condition und Deadlock-Situationen müssen auf logischer Ebene erkannt werden. Erfolgreiche Testläufe sind kein Beweis dafür, dass sie nicht auftreten können.

Eine Deadlock-Situation ist abhängig vom Scheduling und muss nicht auftreten. Erfolgreiche Testläufe schließen Deadlock-Situationen nicht aus.10 


Galileo Computing

9.7 Vermeidungsstrategien zu Deadlocks  downtop

Die Vermeidung von Deadlocks gehört zu den gewichtigeren Problemen im Umgang mit Threads. Deshalb abschließend zu diesem Thema zwei wichtige Regeln.


Galileo Computing

9.7.1 Ressourcen-Anordnung bei Locks  downtop

Transaktion: logisch zusammenhängende Operation

In der Sprache der Datenbanken versteht man unter einer Transaktion eine logisch zusammenhängende Arbeitseinheit, die – etwas vereinfacht ausgedrückt – entweder ganz oder gar nicht ausgeführt wird.

In Java besteht eine Transaktion aus einer zusammenhängenden Sequenz von Operationen, an denen zumindestens zwei Objekte beteiligt sind. Diese Sequenz läuft in einem Thread exklusiv ohne Beeinflussung durch andere Threads ab.

Deadlock durch falsche Lock-
Reihenfolge

Deadlocks können bei Transaktionen immer dann auftreten, wenn Threads die Exklusivität dadurch sichern, dass sie zuerst die Locks der beteiligten Objekte erwerben, um anschließend die Operationen auszuführen.

Die im Beispiel von 9.6.3 provozierte Deadlock-Situation hätte vermieden werden können, wenn die Lock-Reihenfolge der beiden Threads einheitlich gewesen wäre.

Icon

Es gilt die einfache Regel:

Ressourcen-Anordnung bei Lock-Reihenfolge

gp  Sind alle an der Transaktion beteiligten synchronisierten Objekte bekannt, lässt sich mit Hilfe von Ressourcen-Anordnung (Resouce-Ordering) ein Deadlock aufgrund falscher Sperrfolge vermeiden.11 

Resource-Ordering

Unter Ressourcen-Anordnung versteht man die Nummerierung der Objekte, die an der Transaktion beteiligt sind, mit einem anschließenden Lock-Versuch in auf- oder absteigender Nummernfolge.

Dies verhindert Deadlocks durch falsche Sperrfolgen, denn:

Einheitliche
Sperrfolge durch Resource-Ordering

gp  Muss jeder Thread die Locks für gemeinsam genutzte Objekte in der gleichen Reihenfolge erwerben, werden alle bis auf den Thread, der als erster das Objekt mit der kleinsten Nummer gesperrt hat, suspendiert.

Die Klasse Test in 9.6.3 hatte gegen diese Resource-Ordering-Regel verstoßen, weshalb der Deadlock entstanden ist.

Icon

Neben konkreten, am Problem orientierten Lösungen gibt es eine erstaunlich einfache generelle Methodik in Java, den geordneten sequenziellen Zugriff auf Objekte sicherzustellen.

Hashcode für Resource-Ordering

gp  Man nimmt den Hashcode jedes Objekts als Kriterium für die Ressourcen-Anordnung.12 

Die Resource-Ordering-Regel kann leider nicht immer angewendet werden, da auf Transaktions-Ebene nur in wenigen Fällen Kenntnis darüber vorliegt, welche Objekte mit welchen synchronisierten Methoden überhaupt an der Transaktion beteiligt sind und ob es Abhängigkeiten gibt, die einzuhalten sind (siehe nächsten Abschnitt).


Galileo Computing

9.7.2 Deadlocks in Objekt-Kompositionen  downtop

Bei Objekt-Kompositionen kann die Regel der Ressourcen-Anordnung nicht immer eingehalten werden und selbst wenn, kann trotzdem noch ein Deadlock entstehen.

Nested-Monitor-Problem
Icon

Das Problem – auch als Nested-Monitor-Problem bekannt – kann wie folgt beschrieben werden.

gp  Bei Objekt-Kompositionen führen synchronisierte Methoden von inneren Objekten, die ein wait() enthalten13  , zu einem Deadlock, wenn sie von einer synchronisierten Methode des äußeren Objekts aufgerufen werden.

Denn wird durch wait() ein Thread im inneren Objekt blockiert, gibt er nicht das Lock des äußeren frei. Damit ist kein anderer Thread in der Lage, die Suspendierung durch wait() aufzuheben, da das innere Objekt nur über das äußere erreicht werden kann.

Icon

Die Regel besteht nun einfach darin, diese Situationen zu vermeiden:

Vermeidungsstrategie:
Nested-Monitor-Problems

gp  Muss die Methode des äußeren Objekts synchronisiert sein, so ist Komposition durch Aggregation zu ersetzen.
gp  Muss die Komposition erhalten bleiben, dürfen diejenigen Methoden des äußeren Objekts nicht synchronisiert werden, die synchronsierte Methoden mit wait() der inneren Objekte aufrufen.

Galileo Computing

9.8 Guarded-Method: wait() und notify()  downtop

Für eine Inter-Thread-Kommunikation wird in vielen Fällen die Methode Object.wait() verwendet.

Icon

Der Einsatz von wait() erfordert dann zwangsläufig den von notify() bzw. notifyAll().

notify() vs.
notifyAll()

gp  Normalerweise wird das Paar wait() und notifyAll() eingesetzt, da notify() nur eine zeitoptimierte Version von notifyAll() darstellt, die zu Deadlocks führen kann.

Icon
wait()-Wirkung

Die Wirkung von wait() ist nicht unbedingt einfach zu verstehen. Deshalb nachfolgend die Regeln zum Ablauf:

1. Der Thread, der wait() ausführt, muss das Lock besitzen, d.h., wait() kann nur in synchronisiertem Code ausgeführt werden.
2. wait() gibt das Lock nur für das Objekt zurück, von dem es aufgerufen wurde und suspendiert den laufenden Thread.
3. Wird der suspendierte Thread wieder lauffähig, muss er zuerst das Lock zum Objekt erwerben, um dann mit der Anweisung nach wait() fortzufahren.

Galileo Computing

9.8.1 Auswirkung der wait()-Regel  downtop

Die Ablaufregel zu wait() hat subtile Auswirkungen, die näher besprochen werden sollen.

Missachtung der wait()-Regel: Deadlock

Die Nichtbeachtung des zweiten Punkts der wait()-Regel kann zu Deadlocks führen. Man betrachte dazu folgenden Code:

wait() gibt nur ein Lock frei

public synchronized void f(SyncObj 
sobj) {
  synchronized (sobj) {
      try { wait(); } catch (InterruptedException e) {}
      //...
  }
}

Die Betonung im zweiten Schritt liegt auf dem »nur« bei Lock. Im o.a. Code wird nur das Lock von this freigegeben, nicht aber das von sobj.

Das Lock von sobj geht also mit in den Wartezustand. Sollte ein anderer Thread auch auf das Lock von sobj warten, um danach eine notify() ausführen zu wollen, hat man wieder einen Deadlock erzeugt.

wait(): nicht atomarer Ablauf

wait()-Code ist nicht atomar

Der dritte Schritt der wait()-Regel besagt nichts anderes, als dass synchronisierter Code, der ein wait() ausführt, nicht atomar ist.

Beispiel

Konzeptionelle Sicht zur wait()-Anweisung

public synchronized void service() {
  // Anweisungen1
  if (!condition)  
   // wenn Bedingung nicht gilt, dann warten
try { wait(); } catch (InterruptedException e) {}
  // Anweisungen2
}

Dieser Block ist – sofern die Bedingung condition den Wert false ergibt – konzeptionell äquivalent zu folgendem (Pseudo-)Code:

public void service() {
  synchronized(this) {
    // Anweisungen1
}
  --- suspendiert ohne Lock ---
  // Zustände vor Lock können 
hier bereits geändert sein! 
  synchronized(this) {
    // Anweisungen2
} }

Im suspendierten Zustand können andere Threads alle Methoden desselben Objekts benutzen. Dies ist sogar erforderlich, da sonst kein notify() erfolgen könnte.


Galileo Computing

9.8.2 Wait-Regel bzw. -Idiom  downtop

Icon

Dies führt zu einer Regel mit nachfolgendem Idiom.

Objekt-Zustand nach wait()

gp  Nach der Wait-Suspendierung kann ein Thread keine Annahmen über die Zustände des Objekts machen, die vor wait() gültig waren.

Das heißt unter anderem, dass die Bedingung condition nach wait() nicht unbedingt true ist. Denn da der Thread nach der Suspendierung zuerst in den Zustand »lauffähig« versetzt wird, kann vorher ein anderer Thread vom Scheduler in den Zustand »läuft« versetzt werden und die condition wieder auf false setzen.

Icon

Dies führt direkt zu einem Code-Muster für wait():

Test der
wait()-Bedingung

    while (!condition)
      try { wait(); } catch (InterruptedException e) {}

Nach der Suspendierung wird – im Gegensatz zur if-Lösung – die Bedingung wieder geprüft.


Galileo Computing

9.8.3 Guarded-Method und Guarded-Action-Idiom  downtop

Das Muster »so lange warten, bis Bedingung/Zustand eintritt« führt zum Begriff des »guarded« bzw. »überwachten Codes«:

gp  Unter einer Guarded-Method bzw. einem Guarded-Block versteht man eine synchronisierte Aktion, die nur aufgrund einer bestimmten Bedingung bzw. eines Zustands ausgeführt werden darf.

Die Bedingung bzw. der Zustand muss dann zwangsläufig von mehr als einem Thread beeinflusst werden, da er ja ansonsten nie eintreten würde.

Icon
Guarded-Actions

Nachfolgend ein Idiom für eine Klasse mit mehreren Zuständen, welches ein Muster für Guarded-Actions enthält.

class GuardedIdiom {
  private int state;         // enthält Zustand des 
Objekts
  private synchronized void checkState() {      
  
    int oldState= state;
    // --- hier Code mit einem evtl. Wechsel 
des state ---
    if (oldState!=state)  // hat der Zustand sich 
geändert?
      notifyAll();        // dann alle Wartenden informieren
  }
  public synchronized void guardedAction() {    
  // Muster
    while ( /* --- warten, bis Zustand ok --- */ 
)
      try { wait(); } catch (InterruptedException e) {}
    // --- Zustand ist ok, also Aktion ausführen 
---
    checkState();  // Zustand prüfen, evtl. 
Info an Wartende
  }
  public synchronized void modify() {
    // --- Aktion, die Zustand ändern könnte ---
checkState(); // Zustand prüfen, evtl. Info an Wartende } }

Erklärung: Der Einsatz von wait() in nur einem Thread wäre töricht und das Idiom überflüssig. Denn wer sollte schon den Zustand ändern?

Es muss also zumindest einen zweiten Thread geben, der z.B. modify() ausführt, den Zustand für die guardedAction() positiv ändert und mittels checkState() den suspendierten Thread wieder aktiviert.

Beispiel: Kassieren

Umsetzung des Idioms zu Guarded-Actions

Eine Instanz der »Warteschlange an der Kasse« CashDeskQueue erzeugt in einem Thread CustomerProducer Kunden, die es in einem anderen Thread CashDesk abkassiert. Kunden zur FIFO-Warteschlange hinzufügen bzw. sie abzukassieren sind Guarded-Methods.

Passiver Kunde

class Customer {                      // ein trivialer 
Kunde
  private double toPay;
  public Customer(double toPay) { this.toPay= toPay; }
  public double getAmount()     { return toPay; }
}

Aktive Klasse

class CashDeskQueue {
  private final int MAX;      // maximale Länge der Schlange
  // --- vier Zustände ---
private final int STOP= -1; // Kassieren beenden private final int EMPTY= 0; // kein Kunde wartet private final int GO= 1; // Kunden warten private final int FULL= 2; // Kunden warten, Schlange voll
  private int state= STOP;    // aktueller Zustand
  private List l= new ArrayList(); // Liste als 
Warteschlange
  private int numProducer= 0;      // Anzahl der Erzeuger-Threads   
 
  private int numCustomer;    // Anzahl der zu erzeugenden Kunden
  CashDeskQueue(int max) {MAX= max;}  // Länge 
festlegen: max>0

Prüfen des Zustands

  private synchronized void checkState() {  // siehe 
Idiom
    int oldState= state;
    if (l.isEmpty())                      // Warteschlange leer
      if (numProducer<=0)   state= STOP;  // Kunden-Thread-Ende
      else                  state= EMPTY; // Kunden-Thread aktiv
    else if (l.size()==MAX) state= FULL;  // Warteschlage voll
    else                    state= GO;    // Kunden vorhanden
    if (oldState!=state && oldState!=GO)  // Notify bei Änderung
      notifyAll();                        // und STOP/EMPTY/FULL 
  }
  // --- die Guarded-Action mit wait(), siehe Idiom  ---

Erste Guarded-Action

  public synchronized boolean chargeMore() { // 
weiterkassieren?
    while (state == EMPTY)                   // Warte-Zustand
      try { wait(); } catch (InterruptedException e) {}
    if (state==STOP) return false;         // Kassieren 
beenden
    System.out.println("Kassiert: "+          // 
anzeigen &
       ((Customer)l.remove(0)).getAmount());  // entfernen
    checkState();                          // Zustand 
prüfen
    return true;                           // weiterkassieren
  }
  // --- die modify-Action mit wait(), siehe 
Idiom  ---

Zweite Guarded-Action

  public synchronized void arrive(Customer c) {
    while (state == FULL) {           // Voll: anzeigen & warten
      System.out.println("Kasse voll");
      try { wait(); } catch (InterruptedException e) {}
    }
    l.add(c);                         // Kunde in Warteschlange
    System.out.print("Warten: ");     // Warteschlange anzeigen
    for (Iterator i=l.iterator(); i.hasNext();)
      System.out.print(((Customer)i.next()).getAmount()+" ");
    System.out.println();
    checkState();                     // Zustand prüfen
  }
 // --- die Threads Kundenerzeuger und 
–abkassierer  ---
// --- sind als innere Klassen angelegt ---

Producer-Thread

  private class CustomerProducer extends Thread 
{
    public void run() {
      int n=0;
      while (n++<numCustomer) {
        arrive(new Customer((int)(100*Math.random()+1)));
        try { Thread.sleep((int)(750*(1.+Math.random())));
        } catch (InterruptedException e) {}
      }
synchronized (CashDeskQueue.this) { // Lock: äußeres 
Objekt
        numProducer--; checkState();      // Ende mitteilen       ¨
      } 
    }
  }

Consumer-Thread

  private class CashDesk extends Thread {
    public void run() {
      do {
        try {Thread.sleep(1900);} catch(InterruptedException e){}
      } while (chargeMore());
    }
  }
  // --- Abschließend die Simulation: 
Start der Threads ---

Kassen-Simulation

  public synchronized void simulate(int numCustomer) 
{
    numProducer++; this.numCustomer= numCustomer;
    new CustomerProducer().start(); new CashDesk().start();
  }
}

Das Testprogramm legt eine Instanz von CashDeskQueue an, beschränkt die Warteschlange auf drei Kunden und lässt sechs Kunden erzeugen.

public class Test {
  public static void main(String[] args) {
    CashDeskQueue cdq= new CashDeskQueue(3);
    cdq.simulate(6);   
  }
}

Die Ablauf ist natürlich nicht deterministisch.

Tabelle 9.4   Zwei mögliche Konsol-Ausgaben der Applikation Test
Warten: 12.0
Warten: 12.0 4.0
Kassiert: 12.0
Warten: 4.0 77.0
Kassiert: 4.0
Warten: 77.0 67.0
Warten: 77.0 67.0 51.0
Kassiert: 77.0
Warten: 67.0 51.0 81.0
Kassiert: 67.0
Kassiert: 51.0
Kassiert: 81.0
Warten: 69.0
Warten: 69.0 16.0
Warten: 69.0 16.0 5.0
Kassiert: 69.0
Warten: 16.0 5.0 86.0
Kassiert: 16.0
Warten: 5.0 86.0 58.0
Kasse voll
Kassiert: 5.0
Warten: 86.0 58.0 88.0
Kassiert: 86.0
Kassiert: 58.0
Kassiert: 88.0

Erklärung: Die Klasse CashDeskQueue enthält die Guarded-Methods chargeMore() und arrive() , die beide den Zustand des Objekts ändern können. Somit rufen sie die Methode checkState() am Ende auf, die mittels notifyAll() bei Bedarf wartende Threads informiert.

Würde man den »CashDesk«-Thread als Dämon anlegen, terminiert er zwar automatisch nach der »CunstomerProducer«-Thread, lässt aber leider auch Kunden unbearbeitet.

Um das Programm bei zwei gleichberechtigten Threads zu beenden, wird ein weiterer Zustand STOP eingeführt, der durch die letzten beiden Anweisungen in CustomerProducer.run() aktiviert wird (siehe Zeile ¨).

Asynchrone Methode simulate()

Vorsicht bei
asynchronen Methoden

Diese CashDeskQueue-Version hat noch einen »Haken«.

Die Methode simulate() ist asynchron, d.h. startet bei jedem Aufruf zwei Threads. Sie kann somit vor Beenden der Simulation erneut (mehrfach) parallel von außen aufgerufen werden.

Dank notifyAll() passiert zwar nichts Schlimmes, aber dabei wird leider numCustomer überschrieben.


Galileo Computing

9.9 Weitere thread-sichere Maßnahmen  downtop

Synchronisation ist eine von vielen Maßnahmen, thread-sicher zu programmieren. Die Fragen nach Alternativen oder Maßnahmen, die nachträglich Thread-Sicherheit erzeugen, sind von Interesse.


Galileo Computing

9.9.1 Threads und immutable Objekte  downtop

Icon

Immutable Objekte sind ein Glücksfall für thread-sicheres Programmieren. Denn eine einfache Regel lautet:

Immutable Klassen:
thread-sicher

gp  Alle Methoden einer immutable Klasse sind ohne Synchronisation thread-sicher.

Die Zustände von immutable Objekten werden nur mittels ihrer Konstruktoren gesetzt, die thread-sicher sind, sofern man keine Dummheiten macht (siehe 9.6.1). Für einfache Objekte ist das Immutable-Konstrukt durchaus eine Alternative zur Synchronisation.

Damit sind die String-Klasse und alle Wrapper-Klassen der primitiven Typen thread-sicher.


Galileo Computing

9.9.2 Zustandslose Methoden  downtop

Icon

Zustandslose Methoden arbeiten nur mit ihren Argumenten bzw. lokalen Variablen und immutable Objekten.

Zustandslose Methode:
thread-sicher

gp  Zustandslose Methoden sind thread-sicher, wenn ihre Argumente – sofern vorhanden – einen primitiven Typ haben oder aber selbst thread-sicher sind.

Die meisten zustandslosen Methoden sind statisch, was aber nicht unbedingt sein muss.

Beispiel

class Complex {
  public double re,im;   // nicht thread-sicher
  //...
  public static Complex  invers (double re, double 
im) {
    // lokales Objekt, siehe auch 9.9.3: 
Exklusiver Zugriff
Complex c= new Complex();
    double d= re*re+im*im; 
    c.re=re/d; c.im= -im/d;
    return c;
  }
}

Diese Methode arbeitet nur mit primitiven Parametern und lokalen Variablen, ist also thread-sicher.


Galileo Computing

9.9.3 Thread-sichere Dekoration/Wrapper  downtop

Hat man bereits eine funktionsfähige Klasse, die nicht thread-sicher ist, so kann man sich häufig durch ein einfaches Dekorations-Muster retten.

Icon

Das Dekorator-Pattern wird in seiner vollen Form in 11.7 vorgestellt. Hier wird es nur als äußere Hülle verwendet:

Thread-sichere Dekoration

gp  Ein thread-sicheres äußeres Objekt »Wrapper« enthält das nicht thread-sichere innere und bietet nach außen exakt die gleiche oder eine eingeschränkte Funktionalität des inneren Objekts.

Immutable Wrapper

Eine eingeschränkte Funktionalität läuft meist auf einen immutable Wrapper bzw. Read-Only-Adapter hinaus, d.h. alle Methoden, die den Zustand des inneren Objekts ändern, werden nicht unterstützt und erzeugen eine Ausnahme (UnsupportedOperationException).

Der Weg der Implementierung führt über eine echte oder logische Komposition, wobei echte Komposition am sichersten ist:

Icon

Exklusiver Zugriff

Exklusiver
Objekt-Zugriff: thread-sicher

gp  Das äußere Objekt erschafft das innere und liefert keine Referenz des inneren Objekts nach außen (siehe hierzu unbedingt 9.7.2).

Exklusive Zugriffs-Konvention

Ist dies nicht zu realisieren, hilft nur logische Komposition, die aber auf einer Vereinbarung beruht (siehe auch 8.4.3):

gp  Einem äußeren Objekt wird das innere per Referenz übergeben, und jede noch vorhandene Referenz der Umgebung auf das innere Objekt wird nicht mehr verwendet.

Im folgenden Code-Fragment wird die Komposition dadurch sichergestellt, dass keine Referenz zum thread-unsicheren Objekt außerhalb des Wrappers existiert (siehe Zeile ¨).

class NotThreadSafe { 
  //... 
}
class SynchronizedWrapper {
   SynchronizedWrapper(NotThreadSafe o) { 
      //... 
   }
   //...
}
//  Anlage des Objekts nach exklusiver Zugriffs-Konvention
SynchronizedWrapper sw= new SynchronizedWrapper( new NotThreadSafe() ); ¨

Synchronisierte Wrapper für Kollektionen

Synchronisierte Wrapper für thread-unsichere Kollektionen

Berühmte Vertreter für thread-sichere Dekorationen sind die synchronisierten Wrapper-Kollektionen (siehe hierzu Kapitel 14).

Sie werden mittels synchronized()-Methoden der Collections-Klasse erzeugt. Auch hier kann die exklusive Zugriffs-Konvention eingehalten werden:

List synclist= Collections.synchronizedList(new 
ArrayList());
Map syncmap= Collections.synchronizedMap(new HashMap());

Galileo Computing

9.10 Thread-Mechanismen  downtop

Der Start eines Threads hängt davon ab, welche Sicht man auf Threads hat, d.h., welche Rolle sie im Programm spielen. Es gibt zwei Extreme.


Galileo Computing

9.10.1 Passive Objekte  downtop

Das eine Extrem besteht darin, Threads nur zur Ablaufkontrolle, d.h. zur Koordination von passiven Objekten einzusetzen.

Threads operieren auf passiven Objekten

gp  Passive Objekte sind dadurch gekennzeichnet, dass sie nur auf Methoden-Aufrufe reagieren und der Aufruf synchron abläuft.

Prominentester Vertreter dieser Art von Thread-Einsatz ist der Hauptthread von main(), der einen sequenziellen Ablauf beginnt und als eigenes Thread-Objekt gar nicht in Erscheinung tritt.

Anonyme Runnable-Klasse, ein Thread-Start-Idiom

Zum Start zusätzlicher Threads wurde das klassische zugehörige Start-Muster bereits in Abb. 9.5 als Alternative [A1] vorgestellt.

Icon

Für kurze run()-Methoden ist folgendes Start-Muster geeignet, das auf einer anonymen Klasse basiert, die Runnable implementiert.

Anonyme Thread-Klasse implementiert Runnable

  public static void startNewThread(final Object 
runArg) {
    new Thread ( new Runnable() {
        public void run() {
          //... hier kann runArg verwendet werden
} }).start(); }

Dieses Idiom kennt Variationen und wird auch bei aktiven Objekten verwendet (zum Einsatz des Idioms siehe 9.11.2).


Galileo Computing

9.10.2 Aktive Objekte  downtop

Das andere Extrem besteht darin, jedes Objekt mit einer »eigenen CPU« auszustatten. Zu jedem aktiven Objekt existiert also (mindestens) ein Thread:

Aktive Objekte agieren mit »eigener CPU«

gp  Aktive Objektennen agieren, d.h. eigene und andere Methoden auslösen und/oder erlauben asynchrone Methoden-Aufrufe.

Die meisten aktiven Objekte kontrollieren ihre eigenen Threads, die im Objekt gekapselt und nach außen transparent sind.


Galileo Computing

9.10.3 Aktive Client- und Server-Objekte  downtop

Interaktionen zwischen passiven bzw. aktiven Objekten sind häufig durch ein Client/Server-Verhältnis gekennzeichnet.

Die Begriffe »Client« und »Server« werden häufig nur im Zusammenhang mit Netzwerken und Datenbanken verwendet. Bei Objekten verwenden wir sie mit einer generelleren Bedeutung:

Client- bzw. Server-Objekte

gp  Unter einem Client versteht man ein Objekt, welches eine Aktivität bzw. Aktion ausführen möchte, die ein Server-Objekt anbietet, es fragt also bei einem Server einen Dienst nach.

Passive Clients und Server kommunizieren aufgrund eines gemeinsamen Threads grundsätzlich synchron. Diese Beschränkung entfällt bei Methoden-Aufrufen von aktiven Servern. Die Interaktion wird asynchron und damit auch komplizierter.

Asynchrone Kommunikation: das generelle Konzept

Asynchrone Ausführungen sind grundsätzlich genereller:

Asynchrone
Kommunikation als generelles Konzept

gp  Eine synchrone Ausführung einer Methode kann durch eine asynchrone Ausführung bzw. bei Rückgabe eines Ergebnisses durch zwei asynchrone Ausführungen nachgebildet werden.

Dazu suspendiert die asynchrone Methode den Aufrufer durch einen Guard so lange, bis die Bearbeitung abgeschlossen ist und liefert – im Fall einer Rückgabe – das Ergebnis durch eine weitere Anwort-Methode.

Die Umkehrung gilt nicht, d.h., synchrone Methoden können keine asynchronen nachbilden.

Vorteil der asynchronen Kommunikation

Aktive Objekte
für distributive Systeme

Die Interaktion zwischen Objekten, bei denen zumindest der Server ein aktives Objekt ist, ist wesentlich genereller als ein rein passives Modell und eignet sich hervorragend für distributive Systeme.

Zu den distributiven Systemen zählen eng bzw. lose gekoppelte Systeme wie Mehrprozessor-Maschinen und Netzwerke.

Nachteile der asynchronen Kommunikation

Komplexe Interaktion zwischen aktiven Objekten

Die Interaktion zwischen aktiven Objekten erfordert den Einsatz von thread-sicheren Design-Mustern, wobei Thread-Sicherheit anhand des Designs und nicht durch Testläufe bewiesen wird.


Galileo Computing

9.10.4 Asynchroner Service  downtop

Icon
Asynchrone Dienste:

Es ist hilfreich, asynchrone Dienste anhand einfacher Merkmale zu unterscheiden. Zwei hauptsächliche Unterscheidungsmerkmale sind die Art der Aktivierung und der Service-Typ (siehe Abb. 9.10).

Aktivierungsart

Aktivierungsart: Sie kann

gp  vom Client ausgehen, d.h., der Client erzeugt einen Thread.
gp  im Server-Objekt – völlig transparent nach außen – stattfinden. Der Server kontolliert seine eigenen Threads.

Service-Typ

Service-Typ: Man unterscheidet

gp  autonome Server-Objekte, die während ihrer Lebenszeit permanent ihre Aufgaben durchführen.
gp  Server, deren Methoden nur während ihrer Ausführung asynchron ablaufen.

Abbildung
Abbildung 9.10   Asynchrone Dienste

Aktivierung durch Client oder Server

Client- vs. server-seitige Aktivierung

Client-seitige Aktivierung ist sicherlich sehr flexibel, weil der Client über passive oder aktive Ausführung selbst entscheiden kann.

Server-seitige Aktivierung hat dagegen den Vorteil, dass der Client sich nicht um alle thread-relevanten Mechanismen kümmern muss.

Autonomes Objekt

Autonomes Objekt: asynchroner permanenter Service

Zu autonomen Objekten zählen insbesondere spezielle Multimedia-, Datenbank-, Netzwerk- oder ereignisverarbeitende bzw. -erzeugende Objekte.

Sie nehmen über ihre gesamte Lebenszeit ihre spezifischen Aufgaben zeit- oder ereignisgesteuert asynchron zum restlichen Programmablauf wahr.

Autonomes Objekt: keine asynchronen Methoden

gp  Die direkte Ausführung einzelner Methoden eines autonomen Objekts – sofern überhaupt sinnvoll – läuft allerdings synchron im Thread des Clients (siehe hierzu 9.12.1).

Asynchrone Methode

Asynchrone Methode: Thread »per Service«

Im Gegensatz zu autonomen Objekten kann ein Server so lange passiv sein, bis ein Client eine Methode aufruft. Die Service-Anfrage aktiviert dann einen Thread, d.h., nur die Operation wird asynchron zum Client ausgeführt.

Für Server, die nur aufgrund von Client-Anfragen aktiv sein müssen, ist diese Art zu bevorzugen.

Asynchrone Methoden leiden aber unter Kommunikationsschwierigkeiten. Denn sie müssen dem Client irgendwie das Ende der Bearbeitung oder das Ergebnis übermitteln.

Neben den o.a. »reinen« Client/Server-Beziehungen gibt es natürlich hybride Konstellationen.


Galileo Computing

9.11 Client-aktivierte asynchrone Methoden  downtop

Im Gegensatz zum synchronen Fall kann der Methoden-Aufruf nicht einfach direkt erfolgen. Der Aufruf hat indirekt zu erfolgen, denn zuerst muss ein Thread gestartet werden, der in run() den Service bzw. die Methode ausführen soll.

Problem: Argument-Übergabe und Ergebnis

Problem run():
keine Argumente, kein Ergebnis

Da run() keine Argumente akzeptiert und kein Ergebnis liefert, müssen die Argumente vorher übergeben werden und für run() im Zugriff stehen.

Im Folgenden werden einzelne Muster bzw. Idiome für spezifische Client-Lösungen vorgestellt.


Galileo Computing

9.11.1 One-Shot-Objekt  downtop

One-Shot-Objekt: genau ein asynchroner Service

Ein einfaches Idiom läßt sich für One-Shot-Objekte angeben, die jeweils nur genau einen asynchronen Service ohne Ergebnis kapseln, der dann direkt in run() implementiert werden kann.

Bedingung ist allerdings, dass die Beendigung der Ausführung für den Client irrelevant ist.

Icon
Asynchroner
One-Shot-Service

class OneShot implements Runnable {
  // eventuelle Argumente hier als private 
Felder ablegen
private Object args;
  // Muster anhand eines Arguments
public OneShot(Object args) { this.args= args; //... }
  public void run() {
    // hier Service (mit Hilfe der Argumente) 
ausführen
} }

Client-Aufruf des One-Shot-Services

Die Ausführung erfolgt dann im Client durch die Anweisung:

  new Thread(new OneShot(args)).start();

Galileo Computing

9.11.2 Asynchrone Methode ohne Ergebnis  downtop

Soll ein Client-Objekt von einem passiven Objekt eine beliebige Methode ohne Ergebnis asynchron ausführen, führt der kürzeste Weg über eine Variation des Idioms in 9.10.1.

class Server {
  // passives Objekt mit einer Methode
public void service(Object arg1 /*, ... */) { //... } //... }

Icon

class Client {

Asynchrone Server-Methode ohne Resultat: client-aktiviert

  static void asynchServiceTest(Object arg /*, ...*/ 
) {
    final Object farg= arg;  // nur final Argumente möglich
    // anonyme Klasse führt service() 
asynchron aus
new Thread ( new Runnable() { public void run() { new Server().service(farg); } }).start(); } }

Aufgrund seiner Konstruktion hat das oben dargestellte Muster allerdings folgende Einschränkungen:

gp  final-Argumente: Alle notwendigen Argumente für die asynchron aufzurufende Methode service() des Servers müssen an das anonyme Runnable-Objekt final übergeben werden (siehe hierzu zweite Regel in 8.5).
gp  kein Ergebnis: Aufgrund der final-Restriktion ist das Muster nicht um die Rückgabe eines Ergebnisses zu erweitern.

Fazit

Client-
Aktivierung: wenig Flexibilität, höherer Aufwand

Wie aus den beiden letzten Code-Mustern zu erkennen, hat die Flexibilität der client-seitigen Aktivierung ihren Preis im erhöhten Aufwand des Aufrufs.

Des Weiteren wurde bewusst auf Methoden verzichtet, die an den Client Ergebnisse zurückliefern. Asynchrone Services mit Ergebnissen sind besser mit server-aktivierten Techniken zu lösen. Dies ist Thema des nächsten Abschnitts.


Galileo Computing

9.12 Server-Aktivierung  downtop

Abhängig von der Art des Diensts gibt es wieder verschiedene Muster, von denen drei nachfolgend exemplarisch vorgestellt werden.

Allen Mustern ist gemeinsam, dass der Client keine Threads starten muss, sondern nur gewisse Konventionen einzuhalten hat.


Galileo Computing

9.12.1 Autonomes Objekt  downtop

Autonomes Objekt:
client-unabhängig

Autonome Objekte führen ihren asynchronen Dienst unabhängig von Client-Aufrufen aus und starten diesen idealerweise direkt bei der Anlage im Konstruktor.

Interessant am folgenden Muster ist neben der Kapselung des objekt-eigenen Threads die Art, wie das autonome Objekt sicherstellt, das run() nur von ihm selbst genutzt werden kann.

Icon

Objekteigene Thread-Prüfung

Autonomes Objekt prüft Thread-Identität

1. Das autonome Objekt startet einen Thread im Konstruktor und speichert ihn in einer private deklarierten Referenz.
2. Die (leider nur) public zu deklarierende Methode run() prüft zuerst, ob der aktuell laufende Thread identisch mit dem eigenen ist. Nur dann führt sie ihren Service aus.

Icon
Autonomes Objekt, Code-Muster

class AutonomousObject implements Runnable {
  private Thread ownThread;
  public AutonomousObject() {
   // 1. Schritt: Initialisierung und Thread-Start
(ownThread= new Thread(this)).start(); }
  public void run() {
   // 2, Schritt: Prüfen, ob objekteigener Thread
if (Thread.currentThread()== ownThread) {
      // hier Service
} } }

Galileo Computing

9.12.2 Asynchrone Methode mit Ergebnis  downtop

Asynchrone Server-Methode mit Resultat

Ist die Ausführung einer Methode zeitintensiv, und kann der Client bis zur Lieferung des Ergebnisses weiter Aufgaben verrichten, sollte man eine server-aktivierte Lösung in Betracht ziehen.

Da nun der Aufruf der Methode von der Rückgabe des Resultats entkoppelt ist, besteht ein interessantes Detail der Lösung in der Frage, wie der Client davon erfährt, dass das Ergebnis vorliegt.

Polling:
unattraktive Busy-Wait-Technik

Eine unattraktive Lösung ist sicherlich Polling:

gp  Der Client fragt den Server periodisch ab, ob das Ergebnis vorliegt.

Diese Art von Busy-Wait-Techniken ist – wenn möglich – zu vermeiden. Zu diesem Zweck werden zwei verschiedene Techniken vorgestellt.


Galileo Computing

9.12.3 join(): Warten ohne Polling, sofern notwendig  downtop

Bei dieser Technik lautet die Prämisse: »Parallel weiterarbeiten und nur warten, falls notwendig«.

join(): nur warten, falls notwendig, kein Polling

Der Client ruft die asynchrone Server-Methode auf, führt parallel seine Aktivitäten aus und holt anschließend das Ergebnis ab, wobei er nur dann warten muss, wenn es noch nicht vorliegt.

Die Server-Methode läuft in einem anderen Thread, der nach Ausführung stirbt. Somit lässt sich für das Warten die Methode join() einsetzen. Der Client wird so lange ohne Polling suspendiert, bis das Resultat vorliegt.

Ohne join() müsste der Client wiederholt den Server-Thread mit Hilfe von isAlive() abfragen, ob er noch arbeitet (busy-waiting).

Adapter-Pattern

Icon

Das Muster für den asynchronen Dienst verwendet das Adapter-Pattern.

Adapter-Pattern

gp  Adaptee-Klassen, deren Dienste für Clients nicht so ganz passen, werden mit Hilfe eines Adapters – als Klasse oder Interface – an die Anforderungen der Clients angepasst.

Beispiel

Die Klasse BigInteger aus java.math wird um eine zeitaufwändige Fakultätsberechnung faculty() in BigInWithFaculty erweitert (siehe Abb. 9.11).

Aktiver Adapter –passiver Adaptee:
Interface als Basis

Damit ein Client die Fakultätsberechnung asynchron ausführen kann, wird die »Adaptee-Klasse« BigIntWithFaculty in eine Adapter-Klasse AsyncBigFaculty eingehüllt, die über ein Interface IBigFaculty den Berechnungsstart und das Abholen des Resultats bereitstellt.


Abbildung
Abbildung 9.11   AsyncBigFaculty als aktive Adapter-Klasse

Das Interface IBigFaculty splittet die ursprüngliche Methode faculty() in eine Berechnungs- und eine Ergebnis-Methode und kapselt damit die Tatsache, dass es sich um zwei synchronisierte Implementierungen handelt.14 

Adapter-Interface

interface IBigFaculty {         
  void calcFaculty(int i);      // startet die Berechnung
  BigInteger result();          // holt das Resultat ab
}

Nachfolgend die Implementierung von Adaptee und Adapter:

Passives
Adaptee-Objekt

class BigIntWithFaculty extends BigInteger {   // 
Adaptee
  BigIntWithFaculty(String si) {super(si);}
  private static BigInteger f(BigInteger bi) { // 
Hilfs-Funktion
    // Simulation einer langen Berechnung!
try {Thread.sleep(100);} catch (InterruptedException e){} if (bi.compareTo(ONE)<=0) return ONE; else return f(bi.subtract(ONE)).multiply(bi); // rekursiv }; public BigInteger faculty() {return f(this);} // berechnet si! }

Aktives
Adapter-Objekt

class AsyncBigFaculty implements IBigFaculty, 
Runnable {
  private BigInteger bi;    // Ergebnis
  private Thread thread;    // zugehörige Thread
  private int i;
  public synchronized void calcFaculty(int 
i) {
    if (i<0 || i>1000) throw new IllegalArgumentException("..");
    if (thread!= null) throw new IllegalStateException("..");
    this.i=i;
    (thread= new Thread(this)).start();   // Thread starten
  }                                      

Thread-Identität prüfen

  public void run() {              
    if (Thread.currentThread()==thread)   //  Thread prüfen
      bi= new BigIntWithFaculty(Integer.toString(i)).faculty();
  }

join() wartet auf das Ergebnis

  public synchronized BigInteger result() 
{
    // Schützen vor Aufruf, ohne vorher calcFaculty aufzurufen
if (thread==null)throw new IllegalStateException(".."); try { thread.join(); // wartet nur, wenn Thread isAlive() } catch (InterruptedException e) { bi= null; } thread= null; // Thread ist tot, also null return bi; } }

Erklärung: Beim Aufruf von calcFaculty() wird die Berechnung nur gestartet, wenn nicht bereits eine Berechnung läuft.

Hierzu wird eine Variante der Thread-Prüfung (siehe 9.12.1) verwendet, wobei beim Abholen des Resultats die Thread-Referenz wieder auf null gesetzt wird. Sollte result() aufgerufen werden, ohne dass vorher eine Berechnung durchgeführt wurde, wird dies erkannt.

Aktiver Server: Client muss Konvention einhalten

Der Client hat keine Threads zu starten, sondern sich nur an die Konvention zu halten: erst die Berechnung starten und danach das Ergebnis abholen:

public class Test {
  public static void main(String[] args) {
    IBigFaculty abf= new AsyncBigFaculty();
    abf.calcFaculty(10);
    // Simulation eigener Aktivitäten
for (int i=0; i<100000000; i++);
    System.out.println(abf.result());
  }
}

Galileo Computing

9.12.4 Callback-Technik  downtop

Callback-Technik: Standard für
die Ereignis- Verarbeitung

Bei der Callback-Technik lautet die Prämisse: »Parallel weiterarbeiten und zurückrufen lassen«. Die Technik ist recht flexibel und kann alternativ oder zusätzlich zur join()-basierten Technik eingesetzt werden.

Im einfachsten Fall implementiert der Client ein Callback-Interface, das die Callback-Methode enthält und übergibt sich selbst vor oder beim Aufruf der Methode. Der Server übergibt dann das Ergebnis der Callback-Methode (siehe Abb. 9.12).

Callback-Interface: lose Kopplung zwischen Client und Server


Abbildung
Abbildung 9.12   Callback-Beziehung zwischen Client und Server

Beispiel (Erweiterung von 9.12.3)

Der Server AsyncBigFaculty wird um einen Konstruktor erweitert, dem ein Callback-Interface BigFacultyClient übergeben wird.

Das Callback-Interface des Clients enthält eine Resultat-Methode ähnlich der des Servers. Bei der Anlage des Server-Objekts hat man die Alternative zwischen Callback- oder Join-basierter-Technik.

//  ---      Klassen, ergänzt um Callback-Code 
       ---
interface BigFacultyClient { void result (BigInteger bi); }

Callback anhand einer Fakultätsberechnung

class AsyncBigFaculty implements IBigFaculty, Runnable 
{
  // wie bisher, siehe 9.12.3
private BigFacultyClient client; // zusätzlich Client-Referenz
  public AsyncBigFaculty() {}              
  public AsyncBigFaculty(BigFacultyClient client) {  // 
Callback
this.client= client; }
  public synchronized void calcFaculty(int i) {/* 
wie bisher */}
  public void run() {
    if (Thread.currentThread()==thread) {
      bi= new BigIntWithFaculty(Integer.toString(i)).faculty();
      if (client != null) {
        client.result(bi); thread= null;  // hier Callback       ¨
      }
    }
  }
  public synchronized BigInteger result() {
    if (client!=null) throw new UnsupportedOperationException();
    // wie bisher, siehe 9.12.3
} }
// --- nur zum Testen ---
class BigFacultyTestClient implements BigFacultyClient {
  public void result(BigInteger bi) {  System.out.println(bi);  }
}
public class Test {
  public static void main(String[] args) {
    IBigFaculty bf= new AsyncBigFaculty(new BigFacultyTestClient());
    bf.calcFaculty(10);                  // läuft asynchron zu 
¦
    int i; for (i=0; i<100000000; i++);  // dieser Schleife und
    System.out.println(i);               // Ausgabe            Æ
  }
}

Erklärung: Die Ausgabe in der Zeile ¨ erfolgt in der Regel vor der in Zeile ¦.

Der Aufruf der Callback-Methode des Clients in Zeile Æ erfolgt im Thread des Servers. Der Client wird also nicht automatisch informiert, sondern muss seine eigene Ausführung unterbrechen.

Synchronisation:
join() vs. Callback

Fazit

gp  Bei der join()-basierten Technik findet eine Synchronisation zwischen Client und Server statt, bei der Callback-Technik nicht.

Dies führt zu einem weiteren Thema, der Thread-Kommunikation per interrupt().


Galileo Computing

9.13 Thread-Unterbrechung  downtop

Thread-Kommunikation: Einsatz von interrupt()

Ein schönerer Titel zum Abschnitt würde »Thread-Kommunikation« lauten. Aber diese läuft entweder indirekt über die Objekte mittels wait() und notifyAll() oder über die Instanz-Methoden join() und interrupt(). Es verbleibt, die letzte Methode und ihren Einsatz zu besprechen.

Wie der Name bereits ausdrückt, ist die Methode interrupt() zur generellen Kommunikation zwischen Threads schlecht geeignet.

Sie wird deshalb meistens auch nur dazu verwendet, einen Thread von einem anderen aus zu beenden bzw. abzubrechen. Aber selbst diese Aufgabe ist nicht unbedingt trivial.

Den Abbruch bzw. Tod eines Threads kann man natürlich wesentlich billiger mit der deprecated stop()-Instanz-Methode erreichen, aber leider nicht thread-sicher, und daher ist sie nur in sehr einfachen Fällen einsetzbar.


Galileo Computing

9.13.1 interrupt() und InterruptedException  downtop

Icon

Die Methode interrupt(), angewendet auf einen Thread,

Wirkung von interrupt()

gp  löst eine InterruptedException bei diesem Thread aus, wenn er durch join(), sleep() oder wait() suspendiert ist, wobei die Ausnahme die Suspendierung beendet.
gp  setzt bei diesem Thread ein Interrupt-Flag, wenn der Thread aktiv ist.

InterruptedException vs. Interrupt-Flag

Das Flag kann entweder mit Thread.interrupted() abgefragt werden oder löst bei einer nachfolgenden Suspendierung eine InterruptedException aus, wobei beide das Flag automatisch wieder zurücksetzen.

Mit der Instanz-Methode isInterrupted() kann alternativ das Flag getestet werden, ohne es zurückzusetzen.

Die Methode hat also unterschiedliche Wirkung, je nachdem, ob der Thread suspendiert ist oder nicht.

So viel zur Theorie! Die eigentliche Frage ist, was die Instanz-Methode interrupt() eigentlich semantisch bedeuten soll. Deshalb gehen wir für die weitere Diskussion vereinfacht von folgender Annahme aus:

interrupt()-Mitteilung beendet Thread

gp  Die Methode interrupt() bzw. InterruptedException teilt einer Thread mit, die Ausführung unkritisch zu beenden.15 

1. Fall: InterruptedException

InterruptedException:
unkritisch, da checked

Der erste Fall ist einfach, denn eine InterruptedException ist eine checked Exception und muss in einer try-Anweisung abgefangen werden.

Bei join() wartet man auf den Tod einer anderen Thread, bei wait() auf das Eintreten eines Objekt-Zustands und bei sleep() will man einfach nur »nichts tun«.

Im zugehörigen catch-Block kann entschieden werden, ob man den Thread wirklich beenden will oder kann. Man hat die Wahl zwischen Ignorieren oder Ressourcen freigeben und abbrechen.

Interessant an dem Fall ist nur, ob man die InterruptedException wieder an den zurückgeben soll, der sie ausgelöst hat (siehe 9.13.2).

2. Fall: Test des Interrupt-Flags

Der zweite Fall ist dagegen recht unangenehm. Im Gegensatz zum ersten muss der Thread-Code überhaupt nicht mit Flag-Tests ausgestattet werden. Die Implikation ist:

Check des Interrupt-Flags:
optional, Polling notwendig

gp  Ein Thread ohne unterbrechbare Suspendierung kann nur erreicht bzw. abgebrochen werden, sofern er Flag-Tests durchführt.
gp  Ein Thread kann nur mittels Pollings das Flag testen.

Der letzte Punkt ist die Ursache dafür, dass in Thread-Code kaum Flag-Tests enthalten sind.

Interrupt-Polling: Dies bedeutet viele verstreute Testanweisungen des Flags (Spagetti-Code) oder die Einbettung zumindest von Teilen des Thread-Codes in Schleifen, die wiederholt das Flag testen.

Beispiele

Unterbrechbare Aktivität eines Servers

Die Klasse Test startet einen InterruptableServer, der so lange läuft, bis er von Test unterbrochen wird. Da der Server keine suspendierende Methode enthält, testet er das Interrupt-Flag mittels Pollings.

Interrupt-Flag:
Polling im Einsatz

class InterruptableServer implements Runnable {
  void operateOnResource() {
     while (!Thread.interrupted()) {
       // Simulation harter Arbeit
for (int i=0; i<500000000; i++); System.out.println("operateOnResource"); } }
  void closeResource() {
    System.out.println("closeResource");
  }
  public void run() {
    operateOnResource(); closeResource();
  }
}
public class Test {
  public static void main(String[] args) {
    Thread t= new Thread(new InterruptableServer());
    t.start();
    System.out.println("Stop Server mit [RETURN]");
    try { System.in.read(); } catch (IOException e) {}
    t.interrupt();
  }
}
Tabelle 9.5   Mögliche Konsol-Ausgabe zum InterruptableServer

Stop Server mit [RETURN]

operateOnResource
closeResource


Eine häufige Variation besteht darin, dass der Server seine Tätigkeit nicht sofort beendet, sondern erst dann, wenn es ihm passt. Die Klasse ReactableServer simuliert dazu einen passenden Zeitpunkt:

Server bestimmt Ende aufgrund eigenen Zustands

class ReactableServer implements Runnable {
  public void run () {
    // Flag: true= Client wünscht Ende des Server-Diensts!
boolean shutDown= false; // So lange busy, kein Ende möglich
boolean busy= true; long time=0;
     while (busy) {
       try {
         Thread.sleep(1000);
         System.out.println("operating");
       } catch (InterruptedException e) {
           shutDown= true;
           time= System.currentTimeMillis();
       }
       if (shutDown)
        // simuliert weitere Arbeiten, bis Ende möglich
busy= System.currentTimeMillis()-time <3000 } System.out.println("closing"); } }

Die Klasse Test ist äquivalent zum vorherigen Beispiel und wird deshalb weggelassen. Nach dem Interrupt arbeitet der Server noch drei »Runden«.

Tabelle 9.6   Mögliche Konsol-Ausgabe zum ReactableServer
Stop Server mit [RETURN]
operating
operating
operating
operating
closing


Galileo Computing

9.13.2 interrupt()-Probleme und Gegenmaßnahmen  downtop

Probleme bei interrupt()

Die Methode interrupt() ist nicht ohne Probleme. Diese treten dann auf, wenn mehrere Objekte zusammenarbeiten und man einen bestimmten asynchronen Dienst beenden will.

Beispiel

In der Klasse Test soll – äquivalent zu den beiden Beispielen in 9.13.1 – der Thread zu InterruptDemo beendet werden.

InterruptDemo berechnet in einer Schleife Fakultäten mit Hilfe einer Instanz von BigIntWithFaculty (siehe 9.12.2) und testet wie im ersten Beispiel 9.13.1 das Interrupt-Flag, um zu beenden.

Versuch
einer Thread-Kommunikation per interrupt() Icon

class InterruptDemo implements Runnable {
  public void run() {
    int i= 0;
    while (!Thread.interrupted()) {
      System.out.println(
        new BigIntWithFaculty(Integer.toString(i++)).faculty());
    }
  }
}

Die Klasse Test ist äquivalent zu den vorherigen Beispielen, wird aber der Vollständigkeit halber noch einmal wiederholt:

public class Test {
  public static void main(String[] args) {
    Thread t= new Thread(new InterruptDemo()); t.start();
    System.out.println("Ende der Berechnung mit [RETURN]");
    try { System.in.read(); } catch (IOException e) {}
    t.interrupt();
  }
}

Problem:
verlorener Interrupt

Ergebnis: Obwohl t.interrupt() ausgeführt wird, terminiert der Thread t nicht, er läuft endlos.

Grund:
suspendiertes passives Objekt

Erklärung: In BigIntWithFaculty wurde eine sleep()-Methode verwendet, die durch t.interrupt() abgebrochen wurde und das Interrupt-Flag zurückgesetzt hat. Der Flag-Test in InterruptDemo ergibt somit false.

Problem

gp  Der Anwender von Objekt-Methoden kann nicht erkennen, ob eine InterruptedException bzw. Interrupt-Flag abgefangen wird.
gp  Selbst wenn der Anwender dies erkennt, kann er interrupt() nicht mehr wie gewohnt für seine Zwecke einsetzen.

1. Lösung: Idiom für das Service-Objekt

Icon

Die Lösung des Problems ist eigentlich einfach, sofern sich das Service-Objekt an folgendes Idiom hält:

InterruptedException in passiven Objekten

gp  Passive Objekte, die suspendierende Methoden einsetzen, sollten im catch-Block InterruptedException »nach oben« weiterreichen:
    try { 
      /* suspendierende Methode */ 
    } catch (InterruptedException e) {
      // erneute Auslösung des Interrupts
Thread.currentThread().interrupt(); }

Hätte das Service-Objekt BigIntWithFaculty dieses Idiom eingehalten, wäre es also zu keinem Problem gekommen.

2. Lösung: client-seitiger Dämon

Wenn alles
nicht hilft: client-seitiger Dämon

Alternativ kann der Client alle kritischen Methoden-Aufrufe von passiven Objekten in eigenen Dämon-Threads laufen lassen, was die Sache für den Client natürlich aufwändig macht.

Beispiel

Für InterruptDemo sieht die Lösung dann wie folgt aus:

class InterruptDemo implements Runnable {
  public void run() {
    int i= 0;
    while (!Thread.interrupted()) {
      final String s= Integer.toString(i++);
    // Einsatz einer anonymen Klasse, um das passive
// Objekt in einen eigenen Dämon-Thread zu kapseln
Thread t= new Thread ( new Runnable() { public void run() { System.out.println(new BigIntWithFaculty(s).faculty()); } }); t.setDaemon(true); t.start(); } } }

Galileo Computing

9.13.3 interrupt()-Konzept  downtop

Ausnahmen wurden in Java nur dafür konzipiert, Fehler zu behandeln, die aber den normalen Programmablauf abbrechen (siehe hierzu die Diskussion in 7.1).

Mit interrupt() wird durch die Hintertür InterruptedException als Kommunikations-Mechanismus eingeführt.

Kommunikation vs. interrupt()-Konzept

Besonders merkwürdig ist, dass InterruptedException nicht wie alle anderen Ausnahmen den normalen Programmablauf abbricht, sondern nur drei suspendierende Methoden.

Für den normalen Programmablauf wird ein Interrupt-Flag gesetzt, das optional per Polling abgefragt werden muss.

Selbst für einen geordneten Abbruch eines Threads ist dieses Konzept nicht sonderlich brillant. Dies merkt man auch an den Interrupt-Diskussionen und dem zögerlichen Einsatz des Mechanismus.

Für weitergehende Thread-Kommunikation, die hier nicht behandelt wurde, erscheint der ganze Mechanismus ohnehin fragil.16 


Galileo Computing

9.14 Unbehandelte Ausnahmen in Threads  downtop

Als Letztes soll kurz die Behandlung von (normalen) Ausnahmen in Threads angesprochen werden.

Icon

Arbeitet man in einer multi-threaded Umgebung, muss man damit rechnen, das fremde Threads unbehandelte Ausnahmen erzeugen können. Hierzu gibt es eine einfache Regel:

Unbehandelte Ausnahme im Thread

gp  Eine unbehandelte Ausnahme beendet den Thread, in dem sie aufgetreten ist, mit einer Stacktrace-Meldung.17 

Das mag für Programmierer im Teststadium ganz nett sein, jedoch nicht unbedingt für den normalen Benutzer. Weiterhin – sollte ein Thread sterben – kann es durchaus notwendig sein, die anderen unkritisch zu beenden.18 

Damit wäre das Problem umrissen, wobei zuerst eine nahe liegende unpassende Lösung folgt.

Beispiel

class ExceptionDemo implements Runnable {
  private int divisor;
  public ExceptionDemo(int i) { divisor= i; }
  public void run() {
    for (int i= 0; i<3; i++)
      // bei Divisor 0 gibt es eine unbehandelte Exception 

      System.out.print(1/divisor+ " ");
      try { Thread.sleep(1000); } catch (Exception e) {}
    }
  }
}

Abfangen unbehandelter Ausnahmen in anderen Threads
Icon

public class Test {
  public static void main(String[] args) {
    try {
      new Thread(new ExceptionDemo(0)).start();
      new Thread(new ExceptionDemo(1)).start();
      System.out.println("main");
    } catch (Exception e) {
       System.out.println("Ausnahme abgefangen");
    }
  }
}
Tabelle 9.7   Ausgabe zum Test
main
1 java.lang.ArithmeticException: / by zero
       at kap_09.ExceptionDemo.run(Test.java:207)
       at java.lang.Thread.run(Thread.java:484)
1 1

Icon

Erklärung: Der Thread mit einer unbehandelten Ausnahme stirbt mit einer Konsol-Meldung, alle anderen laufen weiter.

Multi-Threading und unbehandelte Ausnahmen

Hieraus folgt eine einfache Regel:

Mit einer try-Anweisung fängt man nur unbehandelte Ausnahmen der eigenen Threads ab, nicht die der anderen.


Galileo Computing

9.14.1 ThreadGroup: uncaughtException()  downtop

Threads gehören zu einer
Thread-Gruppe

Alle Threads gehören einer Thread-Gruppe an. Die Default-Thread-Gruppe hört auf den Namen »main«. Hierzu gehören alle Threads, die ohne explizite Angabe der Thread-Gruppe gestartet werden.

uncaughtException()

Sterben Threads aufgrund einer unbehandelten Ausnahme e, wird vorher die Methode uncaughtException() der zugehörigen Instanz von ThreadGroup aufgerufen und die führt ihrerseits – man ahnt es bereits – die Anweisung e.printStackTrace(System.err); aus.

Damit ist die Lösung klar:

Overriding von uncaughtException()

gp  Threads mit potenziell unbehandelten Ausnahmen werden mit einer Instanz einer Subklasse von ThreadGroup gestartet, die die Methode uncaughtException() geeignet überschreibt.

Beispiel

Das letzte Beispiel wird nur um eine Subklasse von ThreadGroup erweitert und die Threads entsprechend gestartet:

class CheckedThreadGroup extends ThreadGroup {
  CheckedThreadGroup(String name) { super(name); }

ThreadGroup-
Subklasse: uncaughtException() für unbehandelte Ausnahmen

  public void uncaughtException(Thread 
t, Throwable e) {
   // --- hier geeignete Aktion  ---
System.out.println("Thread is dying: "+ Thread.currentThread().getName()); } }
class ExceptionDemo implements Runnable { /* wie 
oben */ }
public class Test {
  public static void main(String[] args) {
    ThreadGroup ctg= new CheckedThreadGroup("TG1");
    new Thread(ctg,new ExceptionDemo(0),"EDemo0").start();
    new Thread(ctg,new ExceptionDemo(1),"EDemo1").start();
  }
}
Tabelle 9.8   Ausgabe zum Test
Thread is dying: EDemo0
1 1 1


Galileo Computing

9.15 Zusammenfassung  downtop

Threads sind eine faszinierende Ergänzung zu einer Sprache wie Java. Applikationen werden durch Aufteilung in Threads reaktiv und sind in Netzwerkumgebungen bzw. Mehrprozessorsystemen der Standard.

Nach der Einführung mit vielen wichtigen Begriffen und Grundlagen zu Threads werden Thread-Zustände, wichtige Methoden, Race-Conditions, Synchronisation, Deadlocks und ihre Vermeidung wie z.B. Ressourcen-Ordnung besprochen.

Synchronisierte Methoden in Verbindung wait() und notify() führen zu den Guarded-Methoden. Ein Idiom zu den Guarded-Methoden wird anhand eines größeren Beispiels besprochen.

Die zum Standardrepertoire gehörenden thread-sicheren Maßnahmen wie immutable Objekte, zustandslose Methoden und Wrapper-Klassen werden danach kurz vorgestellt.

Eine Klassifizierung der asynchronen Services führt zu den Client/Server-Varianten wie aktiver Client vs. aktiver Server, den autonomen Objekten oder einzelnen asynchronen Methoden. Hierzu werden Idiome und das Adapter-Pattern anhand von kleinen Beispielen eingeführt.

Abschließend wird die Problematik der Thread-Kommunikation anhand von Abbruch-Mitteilungen zwischen Threads besprochen und die Möglichkeit, Ausnahmen in einer multi-threaded Umgebung abzufangen.


Galileo Computing

9.16 Testfragen19    toptop

Zu jeder Frage können jeweils eine oder mehrere Antwort(en) bzw. Aussage(n) richtig sein.

A: run()

B: main()

C: start()

A: Von lauffähig/ready nach tot/dead.

B: Von schlafend/asleep nach läuft/running.

C: Von suspendiert/waiting nach lauffähig/ready.

D: Von läuft/running nach lauffähig/ready.

class Sleepy {
  public static void f() {
    try { Thread.sleep(2000);
    } catch (Exception e) { System.out.println("interrupted"); }
    System.out.println("ausgeschlafen");
  }
}
public class Test {
  public static void main(String[] args) { 
     Sleepy.f();
     Thread.currentThread().interrupt(); 
  }
}

Welche Aussagen sind richtig?

class MyThread extends Thread {
  private Thread t;
  public MyThread(Thread t) { this.t =t; }
  public void run() {
    System.out.println("Start:"+Thread.currentThread().getName());
    if (t!=null) {
      try { t.join(); } catch (Exception e) {}                  ¨
      System.out.println("Ende");
    }
    else while (true);
  }
}
public class Test {
  public static void main(String[] args) {
    Thread t1= new MyThread(null); 
    Thread t2= new MyThread(t1);                                ¦
    t1.start(); t2.start();                                     Æ
  }
}

Welche Aussagen sind richtig?

A: Die Applikation erzeugt beim Lauf eine Ausnahme.

B: Die Applikation terminiert nicht, d.h. läuft immer weiter.

C: Die Ausgabe enthält die Zeile: Start:Thread-0

D: Die Ausgabe enthält die Zeile: Start:Thread-1

E: Die Ausgabe enthält die Zeile: Ende

F: Kommentiert man die Zeile ¨ aus, enthält die Ausgabe die Zeile: Ende

A: In der 5. Aufgabe sind die Anweisungen in Zeile ¦ und Æ äquivalent zu:

    t1.start();
    new MyThread(t1).start();

B: In der 5. Aufgabe sind die Anweisungen in Zeile ¦ und Æ äquivalent zu:

    new MyThread(t1).start();
    t1.start();
class Any {
  private String s;
  public void setNull() { if (s!=null) s=null; }
  public synchronized void show() {
    while (s==null) try  { wait(); } catch (Exception e) {}     ¨
    System.out.println(Thread.currentThread().getName()+":"+s);
    s= null;
  }
  public synchronized void set(String s) {
    this.s= s; notify();                                        ¦
  }
}

Welche Aussagen sind richtig?

// die Klasse Any wurde aus der 7. Aufgabe übernommen!
class Any {
  private String s;
  public void setNull() { if (s!=null) s=null; }
  public synchronized void show() {
    while (s==null) try  { wait(); } catch (Exception e) {}      
    System.out.println(Thread.currentThread().getName()+":"+s); s= null;
  }
  public synchronized void set(String s) { this.s= s; notify(); }
}
class AnyPlay implements Runnable {
  private Any a;  private int p;
  public AnyPlay(Any a, int p) { this.a= a; this.p= p; }
  public void run() {
    int i=0;
    while (i<5) {
      try { Thread.sleep(1000); } catch (Exception e) {}
      switch (p) {
        case 0: a.setNull(); break;
        case 1: a.set(""+i++); break;
        case 2: a.show();
      }
    }
  }
}
public class Test {
  public static void main(String[] args) {
    Any a= new Any();
    for (int i=0; i<3; i++) new Thread(new AnyPlay(a,i)).start();
  }
}

Welche Aussagen sind richtig?

A: Jeder Programmlauf terminiert.

B: Jeder Programmlauf erzeugt dieselbe Ausgabe.

C: Jeder Programmlauf erzeugt dieselbe Anzahl von Ausgabezeilen.

D: Jede Ausgabezeile enthält den String: Thread-2






1    Auch nach dem Tod der eigenen Thread aus einer anderen Thread, z.B. der Hauptthread, allerdings dann eben synchron.

2    Applikation, Applet, Servlet etc.

3    Zu Dämon-Threads siehe 9.4.4.

4    In das Diagramm wurden nicht die beiden deprecated Methoden suspend() und
resume() bzw. der zugehörige Zustand aufgenommen.

5    FIFO: First-In First-Out

6    yield: in der dt. Bedeutung »Vorfahrt gewähren«

7    Windows NT kennt nur sieben Prioritätsstufen.

8    Leider ziehen viele Maßnahmen ihrerseits wieder Probleme nach sich, weshalb es einige Concurrent-Bücher mit Thread-Pattern zu dieser »dark art« gibt.

9    Dies wird häufig als mehrfaches Erhalten des Locks eines Objekts bezeichnet.

10    Unter Windows NT war ein Deadlock im Beispiel abhängig von der Anzahl der Schleifendurchläufe in run() und bei i<10 sehr selten.

11    Leider können Deadlocks auch noch durch Guards – Suspendierung durch wait() – auftreten, die diese Regel nicht abfängt (siehe 9.7.2).

12    Zumindest bei der Default-Implementierung in Object ist sichergestellt, das
der Hashcode für jedes Objekt eine eindeutige ganze Zahl erzeugt.

13    Synchronisierte Methoden mit wait() sind Guarded-Methods (siehe 9.8.3).

14    Ansonsten hätten wir kein Adapter-, sondern ein Proxy (Stellvertreter)-Pattern.

15    Komplexere Kommunikationsaufgaben sind natürlich wesentlich interessanter, überschreiten aber den Rahmen eines Kapitels.

16    Bis zum Beweis des Gegenteils reicht dieser Abschnitt.

17    Wie bei dem Hauptthread main() wird eine Konsol-Meldung mit Hilfe der Methode printStackTrace() erzeugt.

18    Dazu zählt eine benutzerfreundliche Info und das Schließen aller Ressourcen.

19    Da sich die Testfragen an der Zertifizierung orientieren, decken sie nur einen unwesentlichen Teil des Kapitels ab. Es wird nur Basiswissen zu Threads erwartet.

  

Perl – Der Einstieg




Copyright © Galileo Press GmbH 2001 - 2002
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken und speichern. 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.
Die Veröffentlichung der Inhalte oder Teilen davon bedarf der ausdrücklichen schriftlichen Genehmigung von Galileo Press. Falls Sie Interesse daran haben sollten, die Inhalte auf Ihrer Website oder einer CD anzubieten, melden Sie sich bitte bei: stefan.krumbiegel@galileo-press.de


[Galileo Computing]

Galileo Press GmbH, Gartenstraße 24, 53229 Bonn, fon: 0228.42150.0, fax 0228.42150.77, info@galileo-press.de