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.
9.1 Grundlegende Begriffe
9.1.1 Multi-Tasking, Multi-Threading
Von Betriebssystemen (OS) her kennt man zwei grundlegende Begriffe:
Multi-Tasking
|
Unter Multi-Tasking versteht man die quasi-parallele Ausführung von Programmen unter einem OS (siehe Abb. 9.1). |
Multi-Threading
|
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 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 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.
9.1.2 Scheduling, Priorität und Preemption
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
|
Mit Scheduling bezeichnet man den Ausführungsplan zum Umschalten der aktiven Threads. Scheduling beruht auf Preemption und/oder Time-Slicing. |
Priorität – Preemption
|
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
|
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 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.
9.1.3 Synchronisation
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
|
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 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.
9.2 Thread-Start
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 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 9.6 Service-Klasse als Sub-Thread vs.»mit asynchronem Dienst«
9.2.1 Methoden run() und start()
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.
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-Muster1
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
|
Eine Java-App2 läuft so lange, bis der letzte Nicht-Dämon-Thread3 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.
9.3 Thread-Zustände
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 9.7 Diagramm der Thread-Zustände4
Zustand: aktiv bzw. tot
9.3.1 Zustand: aktiv bzw. tot
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.
9.3.2 Zustand: schlafend
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.
9.3.3 Zustand: blockiert vs. nicht blockiert bei I/O
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
|
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. |
9.3.4 Zustand: Warten auf Lock
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.
9.3.5 Monitor
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
|
Ein Objekt, das die auf ihm operierenden Threads suspendieren und wieder aktivieren kann, heisst Monitor. |
|
Jedes Objekt, das synchronisierten Code enthält, ist ein Monitor. |
9.3.6 Methoden wait(), notify() bzw. notifyAll()
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 9.8 Objekt als Monitor mit Methoden wait() und notify()
9.3.7 Zustand: wartend
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).
notify(): Verlassen des Wartezustands
Kurz zu den Regeln, verbunden mit notify():
|
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
|
Es gibt bei notify() keine Reihenfolge à la FIFO (First-In First-Out5 ). |
|
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.
9.3.8 Unterbrechen der Zustände
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
|
suspendierten Thread einen Übergang nach »aktiv« und die Auslösung einer InterruptedException, die – da keine Runtime-Exception – abgefangen werden muss. |
Interrupt-Flag
|
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.
9.4 Thread-Methoden
Die Klasse Thread enthält als zentraler Repräsentant weitere wichtige Methoden, die kurz vorgestellt werden sollen.
9.4.1 Konstruktoren
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
|
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);
9.4.2 Statische Methoden
Statische Methoden der Klasse Thread
Es folgen einige wichtige statische Methoden:
|
static int activeCount(): Ermittelt die Anzahl der zur Zeit aktiven Threads der aktuellen Thread-Gruppe. |
|
static Thread currentThread(): Ermittelt den zur Zeit laufenden (code-ausführenden) Thread. |
|
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). |
|
static void sleep(long ms) throws InterruptedException: Der laufende Thread wird für die angegebene Zeit in Millisekunden in den Zustand »schlafend« versetzt. |
|
static void yield(): Überlässt einem anderen lauffähigen Thread die Möglichkeit der Ausführung (ist abhängig vom Scheduler!).6 |
9.4.3 Prioritäten
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.
|
Java-Prioritäten können nur im Idealfall 1:1 auf Betriebssystems-Prioritäten abgebildet werden.7 |
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).
Für die Prioriät beim Start gilt folgende Regel:
Default-Priorität eines Threads
|
Ein neuer Thread übernimmt bei der Anlage die Prioriät des Threads, aus dem er erschaffen wurde. |
9.4.4 Instanz-Methoden
Instanz-Methoden der Klasse Thread
Von den insgesamt 20 nicht deprecated Instanz-Methoden werden hier nur die wichtigsten, noch nicht besprochenen vorgestellt.
|
public final int getPriority(),
public final void setPriority(int newPriority): Liest oder setzt die Priorität dieses Threads. |
|
public final ThreadGroup getThreadGroup(): Liest die Thread-Gruppe, zu der dieser Thread gehört. |
|
public void interrupt(): Löst für diesen Thread eine Unterbrechung aus (Details siehe 9.3.8). |
|
public boolean isInterrupted(): Testet für diesen Thread, ob eine Unterbrechung vorliegt, ohne das »unterbrochen«-Flag zu verändern (siehe Alternative interrupted()). |
|
public final native boolean isAlive(): Liefert true, wenn dieser Thread gestartet wurde, aber noch nicht tot ist, ansonsten false. |
|
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
|
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.
9.4.5 Diverse Methoden im Beispiel
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.
9.5 Race-Condition
Wenn Threads parallel auf dieselben Objekte zugreifen können, wird die Frage interessant, in welcher Reihenfolge Lese- und Schreib-Operationen auf den Objekten ablaufen.
9.5.1 Atomare vs. nicht atomare Operationen
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.
9.5.2 Reentrant, Race-Condition und thread-sicher
Im Zusammenhang mit Thread-Problemen sind folgende Begriffe wichtig:
Reentrant Code
|
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
|
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. |
In Verbindung mit Reentrance gilt folgende Regel:
Atomare
Operationen
|
Nur Lese- oder Schreib-Operationen auf Variablen bis vier Byte Länge sind atomar. Alle anderen Operationen sind nicht atomar. |
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
|
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.8
9.6 Synchronisation und Deadlock
Guarded
Concurrency: Single-Thread-Ausführung durch Synchronisation
Mit Hilfe des Schlüsselworts synchronized werden Methoden oder Blocks vor Reentrance geschützt (guarded):
|
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).
9.6.1 Methoden- und Block-Synchronisation
Die Synchronisierung kann auf
Synchronisation einer Methode
|
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
|
Block-Ebene erfolgen: |
synchronized(ReferenzExpression) { synchronizedBlock }
Dann muss der ausführende Thread zuerst das Lock des Objekts erwerben, auf das ReferenzExpression zeigt.
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
|
Die Sychronisation auf Block-Ebene ist feiner und kann die der Methoden emulieren (siehe nachfolgendes Beispiel). |
Beispiel
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
|
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).
9.6.2 Voll- bzw. teilsynchronisierte Objekte
Ein Thread, der den Lock besitzt, kann als einziger beliebige synchronisierte Methoden bzw. Blöcke desselben Objekts ausführen.9
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.
Ist ein Objekt
Thread-Eintritt in Objekten
|
vollsynchronisiert, so kann nur ein Thread zu einem Zeitpunkt auf dem Objekt operieren. |
|
teilsynchronisiert, so können neben dem Thread, der das Lock besitzt, beliebig viele andere Threads unsynchronisierte Methoden parallel im Objekt ausführen. |
9.6.3 Deadlock durch Synchronisation
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.
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 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
|
Synchronisation beseitigt das Problem Race-Conditions und führt dafür zu Deadlock-Problemen. |
Deadlock-sicher nur per Design
|
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
9.7 Vermeidungsstrategien zu Deadlocks
Die Vermeidung von Deadlocks gehört zu den gewichtigeren Problemen im Umgang mit Threads. Deshalb abschließend zu diesem Thema zwei wichtige Regeln.
9.7.1 Ressourcen-Anordnung bei Locks
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.
Es gilt die einfache Regel:
Ressourcen-Anordnung bei Lock-Reihenfolge
|
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
|
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.
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
|
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).
9.7.2 Deadlocks in Objekt-Kompositionen
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
Das Problem – auch als Nested-Monitor-Problem bekannt – kann wie folgt beschrieben werden.
|
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.
Die Regel besteht nun einfach darin, diese Situationen zu vermeiden:
Vermeidungsstrategie:
Nested-Monitor-Problems
|
Muss die Methode des äußeren Objekts synchronisiert sein, so ist Komposition durch Aggregation zu ersetzen. |
|
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. |
9.8 Guarded-Method: wait() und notify()
Für eine Inter-Thread-Kommunikation wird in vielen Fällen die Methode Object.wait() verwendet.
Der Einsatz von wait() erfordert dann zwangsläufig den von notify() bzw. notifyAll().
notify() vs.
notifyAll()
|
Normalerweise wird das Paar wait() und notifyAll() eingesetzt, da notify() nur eine zeitoptimierte Version von notifyAll() darstellt, die zu Deadlocks führen kann. |
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. |
9.8.1 Auswirkung der wait()-Regel
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.
9.8.2 Wait-Regel bzw. -Idiom
Dies führt zu einer Regel mit nachfolgendem Idiom.
Objekt-Zustand nach wait()
|
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.
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.
9.8.3 Guarded-Method und Guarded-Action-Idiom
Das Muster »so lange warten, bis Bedingung/Zustand eintritt« führt zum Begriff des »guarded« bzw. »überwachten Codes«:
|
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.
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.
9.9 Weitere thread-sichere Maßnahmen
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.
9.9.1 Threads und immutable Objekte
Immutable Objekte sind ein Glücksfall für thread-sicheres Programmieren. Denn eine einfache Regel lautet:
Immutable Klassen:
thread-sicher
|
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.
9.9.2 Zustandslose Methoden
Zustandslose Methoden arbeiten nur mit ihren Argumenten bzw. lokalen Variablen und immutable Objekten.
Zustandslose Methode:
thread-sicher
|
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.
9.9.3 Thread-sichere Dekoration/Wrapper
Hat man bereits eine funktionsfähige Klasse, die nicht thread-sicher ist, so kann man sich häufig durch ein einfaches Dekorations-Muster retten.
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
|
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:
Exklusiver Zugriff
Exklusiver
Objekt-Zugriff: thread-sicher
|
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):
|
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());
9.10 Thread-Mechanismen
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.
9.10.1 Passive Objekte
Das eine Extrem besteht darin, Threads nur zur Ablaufkontrolle, d.h. zur Koordination von passiven Objekten einzusetzen.
Threads operieren auf passiven Objekten
|
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.
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).
9.10.2 Aktive Objekte
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«
|
Aktive Objekte können 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.
9.10.3 Aktive Client- und Server-Objekte
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
|
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
|
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.
9.10.4 Asynchroner Service
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
|
vom Client ausgehen, d.h., der Client erzeugt einen Thread. |
|
im Server-Objekt – völlig transparent nach außen – stattfinden. Der Server kontolliert seine eigenen Threads. |
Service-Typ
Service-Typ: Man unterscheidet
|
autonome Server-Objekte, die während ihrer Lebenszeit permanent ihre Aufgaben durchführen. |
|
Server, deren Methoden nur während ihrer Ausführung asynchron ablaufen. |
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
|
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.
9.11 Client-aktivierte asynchrone Methoden
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.
9.11.1 One-Shot-Objekt
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.
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();
9.11.2 Asynchrone Methode ohne Ergebnis
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 /*, ... */) {
//...
}
//...
}
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:
|
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). |
|
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.
9.12 Server-Aktivierung
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.
9.12.1 Autonomes Objekt
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.
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. |
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
}
}
}
9.12.2 Asynchrone Methode mit Ergebnis
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:
|
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.
9.12.3 join(): Warten ohne Polling, sofern notwendig
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
Das Muster für den asynchronen Dienst verwendet das Adapter-Pattern.
Adapter-Pattern
|
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 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());
}
}
9.12.4 Callback-Technik
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 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
|
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().
9.13 Thread-Unterbrechung
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.
9.13.1 interrupt() und InterruptedException
Die Methode interrupt(), angewendet auf einen Thread,
Wirkung von interrupt()
|
löst eine InterruptedException bei diesem Thread aus, wenn er durch join(), sleep() oder wait() suspendiert ist, wobei die Ausnahme die Suspendierung beendet. |
|
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
|
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
|
Ein Thread ohne unterbrechbare Suspendierung kann nur erreicht bzw. abgebrochen werden, sofern er Flag-Tests durchführt. |
|
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
|
9.13.2 interrupt()-Probleme und Gegenmaßnahmen
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()
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
|
Der Anwender von Objekt-Methoden kann nicht erkennen, ob eine InterruptedException bzw. Interrupt-Flag abgefangen wird. |
|
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
Die Lösung des Problems ist eigentlich einfach, sofern sich das Service-Objekt an folgendes Idiom hält:
InterruptedException in passiven Objekten
|
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();
}
}
}
9.13.3 interrupt()-Konzept
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
9.14 Unbehandelte Ausnahmen in Threads
Als Letztes soll kurz die Behandlung von (normalen) Ausnahmen in Threads angesprochen werden.
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
|
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
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
|
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.
9.14.1 ThreadGroup: uncaughtException()
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()
|
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
|
9.15 Zusammenfassung
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.
9.16 Testfragen19
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.
|