Rheinwerk Computing < openbook >


 
Inhaltsverzeichnis
Materialien
Vorwort
1 Java ist auch eine Sprache
2 Imperative Sprachkonzepte
3 Klassen und Objekte
4 Arrays und ihre Anwendungen
5 Der Umgang mit Zeichenketten
6 Eigene Klassen schreiben
7 Objektorientierte Beziehungsfragen
8 Ausnahmen müssen sein
9 Geschachtelte Typen
10 Besondere Typen der Java SE
11 Generics<T>
12 Lambda-Ausdrücke und funktionale Programmierung
13 Architektur, Design und angewandte Objektorientierung
14 Java Platform Module System
15 Die Klassenbibliothek
16 Einführung in die nebenläufige Programmierung
17 Einführung in Datenstrukturen und Algorithmen
18 Einführung in grafische Oberflächen
19 Einführung in Dateien und Datenströme
20 Einführung ins Datenbankmanagement mit JDBC
21 Bits und Bytes, Mathematisches und Geld
22 Testen mit JUnit
23 Die Werkzeuge des JDK
A Java SE-Module und Paketübersicht
Stichwortverzeichnis


Download:

- Listings, ca. 2,7 MB


Buch bestellen
Ihre Meinung?



Spacer
<< zurück
Java ist auch eine Insel von Christian Ullenboom

Einführung, Ausbildung, Praxis
Buch: Java ist auch eine Insel


Java ist auch eine Insel

Pfeil 16 Einführung in die nebenläufige Programmierung
Pfeil 16.1 Nebenläufigkeit und Parallelität
Pfeil 16.1.1 Multitasking, Prozesse und Threads
Pfeil 16.1.2 Threads und Prozesse
Pfeil 16.1.3 Wie nebenläufige Programme die Geschwindigkeit steigern können
Pfeil 16.1.4 Was Java für Nebenläufigkeit alles bietet
Pfeil 16.2 Existierende Threads und neue Threads erzeugen
Pfeil 16.2.1 Main-Thread
Pfeil 16.2.2 Wer bin ich?
Pfeil 16.2.3 Die Schnittstelle Runnable implementieren
Pfeil 16.2.4 Thread mit Runnable starten
Pfeil 16.2.5 Runnable parametrisieren
Pfeil 16.2.6 Die Klasse Thread erweitern
Pfeil 16.3 Thread-Eigenschaften und Zustände
Pfeil 16.3.1 Der Name eines Threads
Pfeil 16.3.2 Die Zustände eines Threads *
Pfeil 16.3.3 Schläfer gesucht
Pfeil 16.3.4 Wann Threads fertig sind
Pfeil 16.3.5 Einen Thread höflich mit Interrupt beenden
Pfeil 16.3.6 Unbehandelte Ausnahmen, Thread-Ende und UncaughtExceptionHandler
Pfeil 16.3.7 Der stop() von außen und die Rettung mit ThreadDeath *
Pfeil 16.3.8 Ein Rendezvous mit join(…) *
Pfeil 16.3.9 Arbeit niederlegen und wieder aufnehmen *
Pfeil 16.3.10 Priorität *
Pfeil 16.4 Der Ausführer (Executor) kommt
Pfeil 16.4.1 Die Schnittstelle Executor
Pfeil 16.4.2 Glücklich in der Gruppe – die Thread-Pools
Pfeil 16.4.3 Threads mit Rückgabe über Callable
Pfeil 16.4.4 Erinnerungen an die Zukunft – die Future-Rückgabe
Pfeil 16.4.5 Mehrere Callable-Objekte abarbeiten
Pfeil 16.4.6 CompletionService und ExecutorCompletionService
Pfeil 16.4.7 ScheduledExecutorService: wiederholende Aufgaben und Zeitsteuerungen
Pfeil 16.4.8 Asynchrones Programmieren mit CompletableFuture (CompletionStage)
Pfeil 16.5 Zum Weiterlesen
 

Zum Seitenanfang

16.3    Thread-Eigenschaften und Zustände Zur vorigen ÜberschriftZur nächsten Überschrift

Ein Thread hat eine ganze Reihe von Zuständen, wie einen Namen und eine Priorität, die sich erfragen und setzen lassen. Nicht jede Eigenschaft ist nach dem Start änderbar, doch welche Eigenschaften änderbar sind, zeigen die folgenden Abschnitte.

 

Zum Seitenanfang

16.3.1    Der Name eines Threads Zur vorigen ÜberschriftZur nächsten Überschrift

Ein Thread hat eine ganze Menge Eigenschaften – wie einen Zustand, eine Priorität und auch einen Namen. Dieser kann mit setName(…) gesetzt und mit getName() erfragt werden:

class java.lang.Thread

implements Runnable
  • Thread(String name)

    Erzeugt ein neues Thread-Objekt und setzt den Namen. Sinnvoll bei Unterklassen, die den Konstruktor über super(name) aufrufen.

  • Thread(Runnable target, String name)

    Erzeugt ein neues Thread-Objekt mit einem Runnable und setzt den Namen.

  • final String getName()

    Liefert den Namen des Threads. Der Name wird im Konstruktor angegeben oder mit setName(…) zugewiesen. Standardmäßig ist der Name »Thread-x«, wobei x eine eindeutige Nummer ist.

  • final void setName(String name)

    Ändert den Namen des Threads.

 

Zum Seitenanfang

16.3.2    Die Zustände eines Threads * Zur vorigen ÜberschriftZur nächsten Überschrift

Bei einem Thread-Exemplar können wir einige Zustände feststellen:

  1. Neu: Der Lebenslauf eines Thread-Objekts beginnt mit new, doch läuft er noch nicht.

  2. Laufend und bereit: Durch start() gelangt der Thread in den Zustand »bereit« bzw. »laufend«. Der Thread wechselt immer zwischen den zwei Zuständen hin und her. Läuft der Thread, hat ihn der Scheduler vom Betriebssystem zur Ausführung ausgewählt; ist er bereit, so läuft er aktuell nicht, wird aber vom Scheduler für die nächste Runde berücksichtigt, bis der Scheduler ihm wieder Rechenzeit zuordnet.

  3. Wartend: Dieser Zustand wird mithilfe spezieller Synchronisationstechniken oder Ein-/Ausgabefunktionen erreicht – der Thread verweilt in einem Wartezustand.

  4. Terminiert: Nachdem die Aktivität des Thread-Objekts beendet wurde, kann es nicht mehr aktiviert werden und ist tot, also beendet.

Mit Lebenszeichen – Zustand über Thread.State

In welchem Zustand ein Thread gerade ist, zeigt die Methode getState(). Sie liefert ein Objekt vom Typ der Aufzählung Thread.State (die einzige Aufzählung im Paket java.lang), das Folgendes deklariert:

Zustand

Erläuterung

NEW

Neuer Thread, noch nicht gestartet

RUNNABLE

Läuft in der JVM.

BLOCKED

Wartet auf einen MonitorLock, wenn er etwa einen synchronized-Block betreten möchte.

WAITING

Wartet etwa auf ein notify().

TIMED_WAITING

Wartet etwa in einem sleep().

TERMINATED

Ausführung ist beendet.

Tabelle 16.2    Zustände eines Threads

Zudem lässt sich die Methode isAlive() verwenden, die erfragt, ob der Thread gestartet wurde, aber noch nicht tot ist.

 

Zum Seitenanfang

16.3.3    Schläfer gesucht Zur vorigen ÜberschriftZur nächsten Überschrift

Manchmal ist es notwendig, einen Thread eine bestimmte Zeit lang anzuhalten. Dazu lassen sich Methoden zweier Klassen nutzen:

  • Die überladene statische Methode Thread.sleep(…): Etwas erstaunlich ist sicherlich, dass sie keine Objektmethode von einem Thread-Objekt ist, sondern eine statische Methode. Ein Grund wäre, dass dadurch verhindert wird, externe Threads zu beeinflussen. Es ist nicht möglich, einen fremden Thread, über dessen Referenz wir verfügen, einfach einige Sekunden lang schlafen zu legen und ihn so von der Ausführung abzuhalten.

  • Die Objektmethode sleep(…) auf einem TimeUnit-Objekt: Auch sie bezieht sich immer auf den ausführenden Thread. Der Vorteil gegenüber sleep(…) ist, dass hier die Zeiteinheiten besser sichtbar sind.

[zB]  Beispiel

Der ausführende Thread soll 2 Sekunden lang schlafen. Wir erreichen das einmal mit Thread.sleep(…):

try {

Thread.sleep( 2000 );

} catch ( InterruptedException e ) { }

Dann mit TimeUnit:

try {

TimeUnit.SECONDS.sleep( 2 );

} catch ( InterruptedException e ) { }

Der Schlaf kann durch eine InterruptedException unterbrochen werden, etwa durch interrupt(). Die Ausnahme muss behandelt werden, da sie keine RuntimeException ist.

class java.lang.Thread

implements Runnable
  • static void sleep(long millis) throws InterruptedException

    Der aktuell ausgeführte Thread wird millis Millisekunden schlafen gelegt; eine kleine Ungenauigkeit ist natürlich möglich. Unterbricht ein anderer Thread den schlafenden, wird vorzeitig eine InterruptedException ausgelöst.

  • static void sleep(long millis, int nanos) throws InterruptedException

    Der aktuell ausgeführte Thread wird millis Millisekunden und zusätzlich nanos Nanosekunden schlafen gelegt. Im Gegensatz zu sleep(long) wird bei einer negativen Millisekundenanzahl eine IllegalArgumentException ausgelöst. Diese Exception wird auch ausgelöst, wenn die Nanosekundenanzahl nicht zwischen 0 und 999.999 liegt.

[+]  Tipp

Um Entwickler von der Nutzung einer veralteten API abzubringen, kann gemeinerweise eine Verzögerung eingebaut werden – schnell wird sich dann die neue API durchsetzen.

enum java.util.concurrent.TimeUnit

extends Enum<TimeUnit>

implements Serializable, Comparable<TimeUnit>
  • NANOSECONDS, MICROSECONDS, MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS

    Aufzählungselemente von TimeUnit

  • void sleep(long timeout) throws InterruptedException

    Führt ein Thread.sleep() für die Zeiteinheit aus.

Eine überladene Methode Thread.sleep(long, TimeUnit) wäre nett – die gibt es aber nicht.

[+]  Tipp

Um nach einer gewissen Zeit etwas zu tun, muss kein eigener Thread gestartet und dann in den Ruhemodus versetzt werden, sondern es können aus der Java-API Timer verwendet werden.

UML-Diagramme für »Timer« und »TimerTask«

Abbildung 16.4    UML-Diagramme für »Timer« und »TimerTask«

 

Zum Seitenanfang

16.3.4    Wann Threads fertig sind Zur vorigen ÜberschriftZur nächsten Überschrift

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

  • Die run()-Methode wurde ohne Fehler beendet. Wenn wir eine Endlosschleife programmieren, würde diese potenziell einen nie endenden Thread bilden.

  • In der run()-Methode tritt eine RuntimeException auf, die die Methode beendet. Das beendet weder die anderen Threads noch die JVM als Ganzes.

  • Der Thread wurde von außen abgebrochen. Dazu dient die prinzipbedingt problematische Methode stop(), von deren Verwendung abgeraten wird und die auch veraltet ist.

  • Die virtuelle Maschine wird beendet und nimmt alle Threads mit ins Grab.

 

Zum Seitenanfang

16.3.5    Einen Thread höflich mit Interrupt beenden Zur vorigen ÜberschriftZur nächsten Überschrift

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

Wenn wir den Thread schon nicht von außen beenden wollen, können wir ihn immerhin bitten, seine Arbeit aufzugeben und den Freitod zu wählen. Periodisch müsste er dann nur überprüfen, ob jemand von außen den Abbruchswunsch geäußert hat.

Die Methoden interrupt() und isInterrupted()

Die Methode interrupt() setzt von außen in einem Thread-Objekt ein internes Flag, das dann in der run()-Methode durch isInterrupted() periodisch abgefragt werden kann.

Das folgende Programm soll jede halbe Sekunde eine Meldung auf dem Bildschirm ausgeben. Nach 2 Sekunden wird der Unterbrechungswunsch mit interrupt() gemeldet. Auf dieses Signal achtet die sonst unendlich laufende Schleife und bricht ab:

Listing 16.6    src/main/java/com/tutego/insel/thread/ThreadusInterruptus.java, main(…)

Runnable killingMeSoftly = () -> {

System.out.println( "Es gibt ein Leben vor dem Tod." );



while ( ! Thread.currentThread().isInterrupted() ) {

System.out.println( "Und er läuft und er läuft und er läuft" );



try {

Thread.sleep( 500 );

}

catch ( InterruptedException e ) {

Thread.currentThread().interrupt();

System.out.println( "Unterbrechung in sleep()" );

}

} // end while



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

};



Thread t = new Thread( killingMeSoftly );

t.start();

Thread.sleep( 2000 );

t.interrupt();

Die Ausgabe zeigt hübsch die Ablaufsequenz:

Es gibt ein Leben vor dem Tod.

Und er läuft und er läuft und er läuft

Und er läuft und er läuft und er läuft

Und er läuft und er läuft und er läuft

Und er läuft und er läuft und er läuft

Unterbrechung in sleep()

Das Ende
[+]  Hinweis

Wird von Thread geerbt, sind die Methoden isInterrupted() und interrupt() sofort in der Unterklasse zugänglich.

Die run()-Methode im Thread ist so implementiert, dass die Schleife genau dann verlassen wird, wenn isInterrupted() den Wert true ergibt, also von außen die interrupt()-Methode für dieses Thread-Exemplar aufgerufen wurde. Genau dies geschieht in der main(…)-Methode.

Auf den ersten Blick ist das Programm leicht verständlich, doch vermutlich erregt das interrupt() im catch-Block Aufmerksamkeit. Stünde diese Zeile dort nicht, würde das Programm aller Wahrscheinlichkeit nach nicht funktionieren. Das Geheimnis ist Folgendes: Wenn die Ausgabe nur jede halbe Sekunde stattfindet, befindet sich der Thread fast die gesamte Zeit über in der Schlafmethode sleep(…). Also wird vermutlich der interrupt() den Thread gerade beim Schlafen stören. Das mag die Methode überhaupt nicht und reagiert – gut dokumentiert in der Javadoc – wir folgt:

  1. sleep(…) löst bei Unterbrechung mit interrupt() eine InterruptedException aus. Das ist genau die Ausnahme, die unser try-catch-Block auffängt.

  2. Die Methode setzt das Interrupt-Flag zurück. Eine Abfrage über isInterrupted() meldet folglich keine Unterbrechung.

Es ist gut, dass das Interrupt-Flag zurückgesetzt wird, denn es hat ja schon seinen Dienst erwiesen. In unserem Fall muss aber erneut interrupt() aufgerufen werden, da das Abbruch-Flag neu gesetzt werden muss, damit isInterrupted() in der while-Schleife das Ende bestimmen kann.

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

[+]  Hinweis

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

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

Die Methodennamen sind verwirrend gewählt, sodass wir die Aufgaben noch einmal zusammenfassen wollen: Die Objektmethode interrupt() setzt in einem (anderen) Thread-Objekt ein Flag, dass es einen Antrag gab, den Thread zu beenden. Sie beendet aber den Thread nicht, obwohl es der Methodenname nahelegt. Dieses Flag lässt sich mit der Objektmethode isInterrupted() abfragen. In der Regel wird dies innerhalb einer Schleife geschehen, die darüber bestimmt, ob die Aktivität des Threads fortgesetzt werden soll. Die statische Methode interrupted() ist zwar auch eine Anfragemethode und testet das entsprechende Flag des aktuell laufenden Threads, wie Thread.currentThread().isInterrupted(), aber zusätzlich löscht es den Interrupt-Status auch, was isInterrupted() nicht tut. Zwei aufeinanderfolgende Aufrufe von interrupted() führen daher zu einem false, es sei denn, in der Zwischenzeit erfolgt eine weitere Unterbrechung.

 

Zum Seitenanfang

16.3.6    Unbehandelte Ausnahmen, Thread-Ende und UncaughtExceptionHandler Zur vorigen ÜberschriftZur nächsten Überschrift

Kommt es bei der Thread-Ausführung zu einer unbehandelten nichtgeprüften Ausnahme, so beendet die JVM den Thread. Das wird nicht gesondert auf der Konsole geloggt, lediglich die Exception-Meldung kommt.

[zB]  Beispiel

Die JVM beendet den Thread aufgrund einer nicht abgefangenen Exception, der main-Thread läuft weiter:

Thread t = new Thread( () -> {

System.out.println( Thread.currentThread() );

System.out.println( 1 / 0 );

}, "Knallerthread" );

t.start();

System.out.println( t.isAlive() );

Thread.sleep( 1000 );

System.out.println( Thread.currentThread() );

System.out.println( t.isAlive() );

Die Ausgabe ist:

true

Thread[Knallerthread,5,main]

Exception in thread "Knallerthread" java.lang.ArithmeticException: / by zero

at T.lambda$0(T.java:8)

at java.base/java.lang.Thread.run(Thread.java:844)

Thread[main,5,main]

false

UncaughtExceptionHandler setzen

Einer der Gründe für das Ende eines Threads ist eine unbehandelte Ausnahme, etwa von einer nicht aufgefangenen RuntimeException. Um in diesem Fall einen kontrollierten Abgang zu ermöglichen, lässt sich an den Thread ein UncaughtExceptionHandler hängen, der immer dann benachrichtigt wird, wenn der Thread wegen einer nicht behandelten Ausnahme endet.

UncaughtExceptionHandler ist eine in Thread deklarierte geschachtelte Schnittstelle, die eine Operation void uncaughtException(Thread t, Throwable e) vorschreibt. Eine Implementierung der Schnittstelle lässt sich entweder an einen individuellen Thread oder an alle Threads anhängen, sodass im Fall des Abbruchs durch unbehandelte Ausnahmen die JVM die Methode uncaughtException(…) aufruft. Auf diese Weise kann die Applikation im letzten Atemzug noch den Fehler loggen, den die JVM über das Throwable e übergibt.

class java.lang.Thread

implements Runnable
  • void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)

    Setzt den UncaughtExceptionHandler für den Thread.

  • Thread.UncaughtExceptionHandler getUncaughtExceptionHandler()

    Liefert den aktuellen UncaughtExceptionHandler.

  • static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)

    Setzt den UncaughtExceptionHandler für alle Threads.

  • static Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler()

    Liefert den zugewiesenen UncaughtExceptionHandler aller Threads.

Ein mit setUncaughtExceptionHandler(…) lokal gesetzter UncaughtExceptionHandler überschreibt den Eintrag für den setDefaultUncaughtExceptionHandler(…). Zwischen dem mit dem Thread assoziierten Handler und dem globalen gibt es noch einen Handler-Typ für Thread-Gruppen, der jedoch seltener verwendet wird.

Wenn der Thread einen Fehler melden soll

Da ein Thread nebenläufig arbeitet, kann die run()-Methode synchron schlecht Exceptions melden oder einen Rückgabewert liefern. Wer sollte auch an welcher Stelle darauf hören? Eine Lösung für das Problem ist ein Listener, der sich beim Thread anmeldet und darüber informiert wird, ob der Thread seine Arbeit machen konnte oder nicht. Eine andere Lösung bietet Callable (siehe Abschnitt 5.5.7, »Gut, dass wir verglichen haben«), mit dem ein spezieller Fehlercode zurückgegeben oder eine Exception angezeigt werden kann.

 

Zum Seitenanfang

16.3.7    Der stop() von außen und die Rettung mit ThreadDeath * Zur vorigen ÜberschriftZur nächsten Überschrift

Wenn ein Thread nicht auf interrupt() hört, aber aus irgendwelchen Gründen dringend beendet werden muss, müssen wir wohl oder übel die veraltete Methode stop() einsetzen.

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

Abbildung 16.5    Dass die Methode »stop()« veraltet ist, zeigen in Eclipse eine unterschlängelte Linie und ein Symbol am linken Rand an. Steht der Cursor auf der problematischen Zeile, weist eine Fehlermeldung ebenfalls auf das Problem hin.

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

class java.lang.Thread

implements Runnable
  • final void stop()

    Wurde der Thread gar nicht gestartet oder ist er bereits abgearbeitet bzw. beendet, kehrt die Methode sofort zurück. Andernfalls wird über checkAccess(…) geprüft, ob wir überhaupt das Recht haben, den Thread abzuwürgen. Dann wird der Thread beendet, egal was er zuvor unternommen hat. Jetzt kann er nur noch sein Testament in Form eines ThreadDeath-Objekts als Exception anzeigen.

Das ThreadDeath-Objekt

So unmöglich ist das Reagieren auf ein stop() auch nicht. Immer dann, wenn ein Thread mit stop() zum Ende kommen soll, löst die JVM eine ThreadDeath-Ausnahme aus, die letztendlich den Thread beendet. ThreadDeath ist eine Unterklasse von Error, das wiederum von Throwable abgeleitet ist, sodass ThreadDeath mit einem try-Block abgefangen werden kann. Die Java-Entwickler haben ThreadDeath nicht zu einer Unterklasse von Exception gemacht, weil sie nicht wollten, dass ThreadDeath bei einer allgemeinen Exception-Behandlung über catch(Exception e) abgefangen wird.[ 250 ](Dass wir die Klasse überhaupt nutzen können, ist einem Fehler von Sun zuzuschreiben; die Klasse sollte eigentlich nicht sichtbar sein. )

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

Listing 16.7    src/main/java/com/tutego/insel/thread/ThreadStopRecovery.java, main(…)

Runnable unstoppable = () -> {

try {

while ( true )

System.out.println( "I Like To Move It." );

}

catch ( ThreadDeath td ) {

System.out.println( "Das Leben ist nicht totzukriegen." );

throw td;

}

};

Thread t = new Thread( unstoppable );

t.start();

try {

Thread.sleep( 2 );

} catch ( Exception e ) { e.printStackTrace(); }

t.stop();

ThreadDeath bietet eine extravagante Möglichkeit, das aktuell laufende Programm zu beenden: throw new ThreadDeath(). Die Anweisung System.exit(int) ist aber weniger aufsehenerregend.

 

Zum Seitenanfang

16.3.8    Ein Rendezvous mit join(…) * Zur vorigen ÜberschriftZur nächsten Überschrift

Wollen wir Aufgaben auf mehrere Threads verteilen, kommt der Zeitpunkt, an dem die Ergebnisse eingesammelt werden. Die Resultate können allerdings erst dann zusammengebracht werden, wenn alle Threads mit ihrer Ausführung fertig sind. Da sie sich zu einem bestimmten Zeitpunkt treffen, heißt das auch Rendezvous.

Zum Warten gibt es mehrere Strategien. Zunächst lässt sich mit Callable arbeiten, um dann mit get() synchron auf das Ende zu warten. Arbeiten wir mit Runnable, so kann ein Thread keine direkten Ergebnisse wie eine Methode nach außen geben, weil die run()-Methode den Ergebnistyp void hat. Da ein nebenläufiger Thread zudem asynchron arbeitet, wissen wir nicht einmal, wann wir das Ergebnis erwarten können.

Die Übertragung von Werten ist kein Problem. Hier können Klassenvariablen und auch Objektvariablen helfen, denn über sie können wir kommunizieren. Jetzt fehlt nur noch, dass wir auf das Ende der Aktivität eines Threads warten können. Das funktioniert mit join(…)-Methoden.

Im Beispiel aus Listing 16.8 legt ein Thread thread in der Variablen result ein Ergebnis ab. Wir können die Auswirkungen von join() sehen, wenn wir die auskommentierte Zeile hineinnehmen:

Listing 16.8    src/main/java/com/tutego/insel/thread/JoinTheThread.java, main(…)

class JoinerRunnable implements Runnable {

public int result;



@Override public void run() {

result = LocalDate.now().getDayOfYear();

}

}



JoinerRunnable runnable = new JoinerRunnable();

Thread thread = new Thread( runnable );

thread.start();

// thread.join();

System.out.println( runnable.result );

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

class java.lang.Thread

implements Runnable
  • final void join() throws InterruptedException

    Der aktuell ausgeführte Thread wartet auf den Thread, für den die Methode aufgerufen wird, bis dieser beendet ist.

  • final void join(long millis) throws InterruptedException

    Wie join(), doch wartet diese Variante höchstens millis Millisekunden. Wurde der Thread bis dahin nicht vollständig beendet, fährt das Programm fort. Auf diese Weise kann versucht werden, innerhalb einer bestimmten Zeitspanne auf den Thread zu warten, sonst aber weiterzumachen. Ist millis gleich 0, so hat dies die gleiche Wirkung wie join().

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

    Wie join(long), jedoch mit potenziell genauerer Angabe der maximalen Wartezeit

Nach einem thread.join(long) ist mitunter die thread.isAlive()-Methode nützlich, denn sie sagt aus, ob thread noch aktiv arbeitet oder beendet ist.

In TimeUnit gibt es mit timedJoin(…) eine Hilfsmethode, die es uns erlaubt, mit der Dauer schöner zu arbeiten.

class java.lang.TimeUnit

implements Runnable
  • void timedJoin(Thread thread, long timeout) throws InterruptedException

    Berechnet aus der TimeUnit und dem timeout Millisekunden (ms) und Nanosekunden (ns) und führt ein join(ms, ns) auf dem thread aus.

Warten auf den Langsamsten

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

[»]  Beispiel

Zwei Threads arbeiten an einem Problem. Anschließend wird gewartet, bis beide ihre Aufgabe erledigt haben. Dann könnte etwa ein anderer Thread die von a und b verwendeten Ressourcen wieder nutzen:

Thread a = new Thread(runnableA);

Thread b = new Thread(runnableB);

a.start();

b.start();

a.join();

b.join();

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

 

Zum Seitenanfang

16.3.9    Arbeit niederlegen und wieder aufnehmen * Zur vorigen ÜberschriftZur nächsten Überschrift

Wollen wir erreichen, dass ein Thread für eine bestimmte Zeit die Arbeit niederlegt und ein anderer den schlafenden Thread wieder aufwecken kann, müssten wir das selbst implementieren. Zwar gibt es mit suspend() und resume() zwei Thread-Methoden, doch diese Start-Stopp-Technik ist deprecated, da sie ähnlich problematisch ist wie stop(). In Zukunft werden diese Methoden aus der Java SE entfernt.

 

Zum Seitenanfang

16.3.10    Priorität * Zur vorigen ÜberschriftZur nächsten Überschrift

Jeder Thread verfügt über eine Priorität, die aussagt, wie viel Rechenzeit ein Thread relativ zu anderen Threads erhält. Die Priorität ist eine Zahl zwischen Thread.MIN_PRIORITY (1) und Thread.MAX_PRIORITY (10). Durch den Wert kann der Scheduler erkennen, welchem Thread er den Vorzug geben soll, wenn mehrere Threads auf Rechenzeit warten. Bei seiner Initialisierung bekommt jeder Thread die Priorität des erzeugenden Threads. Normalerweise ist es die Priorität Thread.NORM_PRIORITY (5).

Das Betriebssystem (oder die JVM) nimmt die Threads immer entsprechend der Priorität aus der Warteschlange heraus (daher Prioritätswarteschlange). Ein Thread mit der Priorität N wird vor alle Threads mit der Wichtigkeit kleiner N, aber hinter diejenigen der Priorität größer gleich N gesetzt. Ruft nun ein kooperativer Thread mit der Priorität N die Methode yield() auf, bekommt ein Thread mit der Priorität <= N auch eine Chance zur Ausführung.

Die Priorität kann durch Aufruf von setPriority(…) geändert und mit getPriority() abgefragt werden. Allerdings macht Java nur sehr schwache Aussagen über die Bedeutung und Auswirkung von Thread-Prioritäten.

[»]  Beispiel

Wir weisen dem Thread t die höchste Priorität zu:

t.setPriority( Thread.MAX_PRIORITY );
class java.lang.Thread

implements Runnable
  • final int getPriority()

    Liefert die Priorität des Threads.

  • final void setPriority(int newPriority)

    Setzt die Priorität des Threads. Die Methode liefert eine IllegalArgumentException, wenn die Priorität nicht zwischen MIN_PRIORITY (1) und MAX_PRIORITY (10) liegt.

Granularität und Vorrang

Die zehn Prioritätsstufen garantieren nicht zwingend unterschiedliche Ausführungen. Obwohl anzunehmen ist, dass ein Thread mit der Priorität NORM_PRIORITY+1 häufiger Programmcode ausführt als ein Thread mit der Priorität NORM_PRIORITY, kann ein Betriebssystem dies anders implementieren. Nehmen wir an, die Plattform implementiert lediglich fünf Prioritätsstufen. Ist 1 die niedrigste Stufe und 5 die höchste – die mittlere Stufe ist 3 –, werden wahrscheinlich NORM_PRIORITY und NORM_PRIORITY + 1 auf die Stufe 3 transformiert und haben demnach dieselbe Priorität. Daraus lernen wir: Auch bei unterschiedlichen Prioritäten können wir nicht erwarten, dass ein bestimmtes Programmstück zwingend schneller läuft. Zudem gibt es Betriebssysteme mit Schedulern, die keine Prioritäten unterstützen oder diese unerwartet interpretieren.

 


Ihre Meinung?

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de

<< zurück
 Zum Rheinwerk-Shop
Zum Rheinwerk-Shop: Java ist auch eine Insel Java ist auch eine Insel

Jetzt Buch bestellen


 Buchempfehlungen
Zum Rheinwerk-Shop: Captain CiaoCiao erobert Java

Captain CiaoCiao erobert Java




Zum Rheinwerk-Shop: Java SE 9 Standard-Bibliothek

Java SE 9 Standard-Bibliothek




Zum Rheinwerk-Shop: Algorithmen in Java

Algorithmen in Java




Zum Rheinwerk-Shop: Objektorientierte Programmierung

Objektorientierte Programmierung




 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und in die Schweiz

InfoInfo



 

 


Copyright © Rheinwerk Verlag GmbH 2021

Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.

Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

 

[Rheinwerk Computing]



Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de



Cookie-Einstellungen ändern