1 Java Threads
Grundlage ist Kapitel 2 des Lehrbuchs (Hettel und Tran (2016))
1.1 Thread Nebenläufigkeit
1.1.1 Nebenläufigkeit
- Programmteil unabhängig von anderen Programmteilen
- kann parallel zu anderen nebenläufigen Teilen abgearbeitet werden
- Wenn zu einem Zeitpunkt mehr nebenläufige Teile zur Abarbeitung anstehen als Kerne (CPU Cores) zur Verfügung stehen, können diese Teile auch durch einen Scheduler durch Wechsel zwischen den Threads überlappend abgearbeitet werden (“Pseudo-Multitasking”).
- JVM startet immer den “main”-Thread, in dem das Hauptprogramm abgearbeitet wird.
- Daneben startet die JVM nebenläufige Teile zur Verwaltung des “main Threads” (z.B. Garbage Collection).
1.1.2 Grundlagen zum Java-Thread
public class MainThread {
public static void main(String... args) {
// Anzahl der Prozessoren abfragen
var nr = Runtime.getRuntime().availableProcessors();
System.out.println("Anzahl der Prozessoren " + nr);
// Eigenschaften des "main Threads"
var self = Thread.currentThread();
System.out.println("Name : " + self.getName());
System.out.println("Prio : " + self.getPriority());
System.out.println("ID : " + self.threadId());
}
}
In Java wird die main
-Methode im sog. “main Thread” ausgeführt. Dieser Thread wird von der JVM automatisch erzeugt.
Java-Threads haben einige Eigenschaften, die auch an Thread
-Objekten abgefragt werden können:
Id
: DieId
ist eine Zahl. In einer Java VM Instanz wird dieId
jeweils nur einmal vergeben. Jeder Thread hat also eine andereId
.Name
: Jeder Thread hat neben der eindeutigen numerischenId
eine textuelle Repräsentation. Wird derName
nicht individuell gesetzt, wird nach einem festen Muster ein textueller Name aus derId
gebildet.Priority
: JedesThread
-Objekt besitzt eine Priorität. Die Priorität entspricht einer Zahl zwischen 1 (Thread.MIN_PRIORITY
) und 10 (Thread.MAX_PRIORITY
) und kann mitsetPriority
gesetzt und mitgetPriority
abgefragt werden. Wird nichts angegeben, erhalten Threads die Standardpriorität 5 (Thread.NORM_PRIORITY
).
Der aktuelle Thread kann immer durch die Klassenmethode currentThread()
von Thread
abgerufen werden.
Die Ausführungsplattform kann Parallelverarbeitungspotenzial haben (muss aber nicht - z.B. auf kleinen Embedded Systemen). Mit der Methode availableProcessors()
kann das an Runtime
-Objekten, die die eigene JVM repräsentieren, erfragt werden.
1.1.2.1 Die Methode availableProcessors
und Hyper-Threading
Auf einem Macbook Pro Retina 2015 ergibt der Aufruf von availableProcessors()
8, obwohl nur 1 CPU vorhanden ist und diese 4 Cores enthält. Durch Hyper-Threading ist die JVM aber in der Lage 8 voneinander unabhängige Abarbeitungs-Engines laufen zu lassen. Zu jedem Zeitpunkt kann die JVM hier 8 Threads parallel ablaufen lassen.
In dieser Abbildung ist:
Runtime.getRuntime().availableProcessors()
\(\to\) 8 wegen Hyper-Threading, obwohl 1 CPU mit 4 Kernen
Hyper-Threading ist ein Feature von manchen Intel- und AMD-Prozessoren: Ein Core besitzt dabei zwei komplette Registersätze (Speicher auf Core für Input, Output und Zwischenergebnisse) und die Verarbeitung erfolgt in Pipeline-Stufen mit zwei parallelen Befehls- und Datenströmen, die gemeinsam die nicht-gedoppelten Ressourcen effizienter ausnutzen.
1.1.3 Zuordnung der Java-Threads zu einzelnen Kernen
Heutzutage stellen alle gängigen Betriebsysteme Threads bereit. Das muss aber nicht so sein bzw. Programme können auch eigene Threadimplementierungen (sog. , vgl.Wikipedia1) nutzen. In obiger Abbildung werden die folgenden Ebenen unterschieden (von oben nach unten):
- Threads auf der Ebene der Java Virtual Machine: Das sind die Java-Threads (Instanzen der Klasse
Thread
). Bei den meisten Implementierungen von Java Virtual Machines (JVM) gibt es auf der Ebene der JVM keinen eigenen Scheduler (denkbar ist das aber). Stattdessen wird für jeden Java-Thread genau ein Thread auf der Ebene des Betriebssystems erzeugt und der Wechsel zwischen den Threads dem Betriebssystem überlassen. - Threads auf der Ebene des Betriebssystems: Die Threads des Betriebssystems gehören jeweils zu einem Prozess. Im Fall der Threads in der Abbildung gehören alle Threads dieser Ebene zum Prozess der JVM. Das Betriebssystem verwaltet diese Threads. Wenn die JVM einen neuen Thread vom Betriebssystem anfordern möchte, muss sie einen entsprechenden System-Call ausführen. Dann erzeugt das Betriebssystem einen neuen Thread zum Prozess der JVM.
- Threads auf der Ebene der Hardware: Die Betriebssystem-Threads werden vom Scheduler des Betriebssystems (OS = Operating System) auf den zur Verfügung stehenden Rechenkernen ausgeführt. Im Allgemeinen wird es weniger Kerne als Threads geben (andere Programme neben der JVM müssen auch mit ihren Threads bzw. als Prozess mit Rechenleistung bedient werden). Der Scheduler des Betriebssystems wechselt in schneller Folge zwischen der Abarbeitung von Threads auf den zur Verfügung stehenden Kernen.
Die Verwendung von Betriebssystem-Threads ist aber nicht unumgänglich. Es gibt Entwicklungen für Java zukünftig feiner granulare nebenläufige Programmteile (sog. bzw. , engl. Faser, also dem Bestandteil von , engl. Faden) mit Betriebssystem-unabhängigen Scheduler zu verwenden (vgl. Project Loom2).
Der Scheduler sollte die Priorität der Threads bei der Zuordnung von Rechenzeit berücksichtigen. Delegiert eine Java Virtual Machine die Scheduling-Funktionalität an das Betriebssystem, wird die Prioriät der Java-Threads (.getPriority()
) allerdings nicht unbedingt an die Betriebssystem-Threads weitergegeben.
1.1.4 Exkurs: Priorität von Betriebssystem-Threads am Beispiel Linux (nicht prüfungsrelevant)
Die folgende Analyse der Priorisierung von Java- bzw. Betriebssystem-Threads wurde auf einem Manjaro System mit Linux-Kernel Version 5.10 …
>> inxi -CS
System: Host: lifebook Kernel: 5.10.70-1-MANJARO x86_64 bits: 64
Desktop: GNOME 40.5 Distro: Manjaro Linux
CPU: Info: Quad Core model: 11th Gen Intel Core i5-1145G7 bits: 64
type: MT MCP cache: L2: 8 MiB Speed: 543 MHz min/max: 400/4400 MHz
… mit OpenJDK Version 17 durchgeführt:
>> java -version
openjdk version "17.0.1" 2021-10-19
OpenJDK Runtime Environment (build 17.0.1+12-39)
OpenJDK 64-Bit Server VM (build 17.0.1+12-39, mixed mode, sharing)
1.1.4.1 Betriebssystem Threads unter POSIX bzw. Linux
POSIX ist ein ISO/IEC (bzw. später IEEE zusammen mit The Open Group) Standard, der Gemeinsamkeiten von UNIX-artigen Betriebssystemen spezifiziert. macOS, AIX, HP-UX und Oracle Solaris sind komplett POSIX-konform, während die meisten Linux-Distributionen nur in (wesentlichen) Teilen POSIX-konform sind. Die POSIX-Teilspezifikation in puncto Threads3 ist Teil der Core Services von POSIX. Sie wird von Linux-Distributionen seit langem umgesetzt. Die GNU C Library (glibc), die Linux verwendet, beinhaltet nämlich für Threads seit der Version 2.3 die NPTL4.
1.1.4.2 POSIX Betriebssystem Threads und ihre Priorität
Mit der NPTL kann die Priorität eines Threads als Teil eines Parameterblocks gesetzt werden. Die Priorität wird allerdings nur von bestimmten Scheduling Policies berücksichtigt (SCHED_RR
und SCHED_FIFO
). Die Default Scheduling Policy (SCHED_OTHER
) verwendet eine einfache “Round Robin”-Strategie, die die Priorität unberücksichtigt lässt.
Die möglichen Scheduling Policies und ihre jeweils erlaubten Prioriäten kann man unter Linux auf der Shell mit dem Kommando chrt -m
ausgeben:
SCHED_OTHER min./max. Priorität : 0/0
SCHED_FIFO min./max. Priorität : 1/99
SCHED_RR min./max. Priorität : 1/99
SCHED_BATCH min./max. Priorität : 0/0
SCHED_IDLE min./max. Priorität : 0/0
SCHED_DEADLINE min./max. Priorität : 0/0
Dabei bedeutet 1
eine niedrige und 99
eine hohe Priorität. Die Policies, die als minimale und maximale Priorität jeweils 0
haben, werten die Priorität nicht aus.
Soll (hier in der Programmiersprache C und der Übersichtlichkeit wegen ohne Fehlerbehandlung) ein Thread für die Funktion func
mit den Aufrufparametern arg
und der Policy SCHED_FIFO
sowie der Priorität 19
gestartet werden, muss zuerst ein pthread_attr_t
Struct durch ein entsprechend parametrisiertes sched_param
Struct, in dem die Priorität als int
repräsentiert ist, und die Policy-Nummer (ebenfalls ein int
) vorbereitet werden.
#include <pthread.h>
#include <sched.h>
pthread_t tid;
pthread_attr_t attr;
sched_param param;
pthread_attr_init(&attr);
pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
param.sched_priority = 19;
pthread_attr_setschedparam(&attr, ¶m);
Der initialisierte sowie mit modifizierter Priorität und Scheduler versehene Attribut Struct attr
kann dann beim Thread Library Call pthread_create
als Parameter übergeben werden:
1.1.4.3 JVM Threads und Betriebssystem Threads unter Linux
Das UNIX-Shell-Kommando ps
(“processes show”) zeigt detailliete Informationen über die laufenden Prozesse, die das Betriebssystem verwaltet. Mit der Kommandozeilenoption -T
kann man nicht nur die Prozesse, sondern auch andere damit in Beziehung stehende Informationen, insbesondere auch die Threads der Prozesse, einsehen.
Die Option -c
zeigt zu jedem Eintrag zusätzlich Scheduler-Informationen an.
Mit der weiteren Opion -p pid
, wobei pid
eine numerische Prozess-ID ist, kann man die Ausgabe auf die Threads genau dieses Prozesses einschränken.
Zusammengefasst kann man unter Linux mit diesem Kommando die Threads des Prozesses pid
mit ihren Scheduling-Informationen anzeigen (>>
ist der “Prompt”).
>> ps -cTp pid
Das folgende Java-Programm macht sich dies zu nutze, indem ein endlos laufender Daemon-Thread erzeugt und gestartet wird, der den Namen NAME
bekommt. Für diesen Thread sollen in dem Programm die Scheduling-Informationen mit ps -cTp
geholt werden, wobei die ID des Prozesses die der Java Virtual Machine ist, die das Java-Programm ausführt. Mit ProcessHandle.current().pid()
kann die pid
der ausführenden JVM abgefragt werden. Die Klasse ProcessHandle
gibt es seit Java 9 im Package java.lang
. Schließlich wird das ps
-Kommando ausgeführt und die Ausgabe zeilenweise auf das Muster (PID | NAME)
durchsucht, wobei NAME
derselbe String
ist, der auch den Namen des gestarteten Threads bildet. Nach “PID
” wird gesucht, damit die erste Zeile mit ausgegeben wird, die Spaltenüberschriften enthält. Java kann seit Version 1.5 externe Programme mit Objekten der Klasse ProcessBuilder
aus dem Package java.lang
starten. Unter POSIX-konformen Betriebssystemen kann als “Kommandoprogramm” die Shell sh
benutzt werden, der man wiederum in ihrer eigenen Syntax die eigentlich auszuführenden Anweisungen übergeben kann.
var t = new Thread(() -> {
while (true) {
// "leere" Endlosschleife
}
}, NAME);
t.setDaemon(true);
t.start();
var pid = ProcessHandle.current().pid();
var exec = new ProcessBuilder("/bin/sh", "-c",
"ps -cT -p " + pid + " | egrep '(PID | " + NAME + ")'");
exec.inheritIO();
exec.start();
Mit NAME = "Rumpelstilzchen"
ergibt sich beispielsweise folgende Ausgabe:
PID SPID CLS PRI TTY TIME CMD
42254 42277 TS 19 ? 00:00:00 Rumpelstilzchen
In dieser Ausgabe beinhaltet die Spalte PID
die Prozess ID der JVM, SPID
ist hier die Thread-ID des von der JVM gestarteten Endlos-Daemon-Threads, CLS
ist die Scheduling-Klasse des Threads (s.u.), PRI
die Priorität des Threads. Sie hat immer den Wert 19
, egal welche Priorität der Java-Thread t
hat. TTY
ist die Nummer der Console, über die der Prozess I/O durchführt. TIME
ist die kumulierte CPU-Zeit, die der Scheduler dem Thread bisher zugewiesen hat (jedoch inkl. blockierter bzw. Wartezeiten). Unter CMD
findet sich im Fall eines Threads sein Name.
Zur Beantwortung der Frage, welches Scheduling-Verfahren TS
ist, bzw. welche sonst noch denkbar wären, liefert die Man-Page von ps
die benötigten Informationen:
>> man ps
[...]
cls CLS Scheduling-Klasse des Prozesses (alias policy,
cls). Mögliche Werte des Feldes sind:
- nicht berichtet
TS SCHED_OTHER
FF SCHED_FIFO
RR SCHED_RR
B SCHED_BATCH
ISO SCHED_ISO
IDL SCHED_IDLE
DLN SCHED_DEADLINE
? unbekannter Wert
[...]
Die Java Virtual Machine erzeugt unter Linux also für jeden Java-Thread einen Betriebssystem-Thread, der mit der Scheduling Policy SCHED_OTHER
betrieben wird. Die Default Scheduling Policy (SCHED_OTHER
) verwendet eine einfache “Round Robin”-Strategie, die die Priorität unberücksichtigt lässt (s.o.). Das erklärt auch, warum die Java-Threadpriorität sich nicht auf die Betriebssystem-Threadpriorität auswirkt.
1.1.5 Exkurs: “Technische” Threads der Java Virtual Machine (nicht prüfungsrelevant bis auf die Tatsache, dass es welche gibt)
“Technische” Threads sind solche Threads, die nicht programmatisch durch Instanziieren von Thread
oder Unterklassen von Thread
erzeugt und gestartet werden, sondern die automatisch von der Java Virtual Machine verwaltet werden. Das sind in der Regel “Daemon” Threads mit sehr hoher Priorität. Sie implementieren Dienste der Java Ablaufumgebung beispielsweise wie die automatische “Garbage Collection” - also das Freigeben von Speicherbereichen von Objekten, die nicht mehr von globalen, lokalen Variablen oder Parametern referenziert werden.
1.1.5.1 getAllThreads()
von Apache Commons Lang
Apache Commons5 ist eine externe Sammlung von wiederverwendbaren Java Komponenten, deren Gebrauch weit verbreitet ist. Apache Commons Lang6 ist eine Bibliothek daraus, die sich mit der Manipulation der Kern-Klassen der Java Standard Library befasst.
Sie enthält u.a. die Klasse ThreadUtils
:
ThreadUtils
stellt die Klassenmethode getAllThreads()
bereit:7
**``public static Collection<Thread> getAllThreads()``**
*Gets all active threads (A thread is active if it has been started and has not yet died).*
**Returns:** *all active threads. The collection returned is always unmodifiable.*
**Throws:**\
- ``SecurityException`` - *if the current thread cannot access the system thread group*\
- ``SecurityException`` - *if the current thread cannot modify thread groups from this thread's thread group up to the system thread group*
getAllThreads()
liefert also eine Colletion
mit allen gerade aktiven Threads. Dazu gehören insbesondere auch die hier interessierenden “technischen” Threads.
1.1.5.2 Implementierung von getAllThreads()
Der ThreadUtils
-Quellcode von Apache Commons Lang kann in Github eingesehen werden8:
- Wenn
ThreadUtils.getAllThreads()
in einem beliebigen Thread aufgerufen wird, holt die Methode sich mitThread.currentThread()
eine Referenz auf diesen Thread. - Von diesem Thread wird dessen Threadgruppe mit
getThreadGroup()
erfragt. - Von dieser
ThreadGroup
wird solange die Eltern-Threadgruppe mitgetParent()
ermittelt, bis es keine Eltern-Threadgruppe mehr gibt, man also die Wurzel-Threadgruppe (“System Threadgruppe”) ermittelt hat. - An der Wurzel-Threadgruppe wird dann die Methode
enumerate(list, true)
9 aufgerufen. Sie kopiert alle aktiven Threads inlist
(vom TypThread[]
). Der zweite Parametertrue
bewirkt, dass alle Untergruppen der Wurzel-Threadgruppe rekursiv auf dieselbe Art behandelt werden. Am Ende sind also alle aktiven Threads inlist
enthalten. - Zum Schluss wird dieses
Thread
-Array in eineList<Thread>
kopiert und ein unmodifiable Wrapper auf diese Liste zurückgegeben.ThreadUtils.getAllThreads()
liefert eine Referenz auf diesen Wrapper als Ergebnis.
1.1.5.3 Verwendung von getAllThreads()
Über die Collection<Thread>
mit Referenzen auf alle aktiven Threads , die der Aufruf von ThreadUtils.getAllThreads()
liefert, wird im folgenden mit einer enhanced for loop iteriert und für jedes Thread
-Objekt t
die Eigenschaften Id
, Name
, Priority
sowie Daemon
abgerufen.
1.1.5.4 Ergebnis von getAllThreads()
: “Technische” Threads bei OpenJDK 16
Beispielhaft wird hier das Ergebnis unter OpenJDK 1610 gezeigt:
ID | Name | Priority | isDaemon |
---|---|---|---|
1 | main | 5 | false |
2 | Reference Handler | 10 | true |
3 | Finalizer | 8 | true |
4 | Signal Dispatcher | 9 | true |
12 | Notification Thread | 9 | true |
13 | Common-Cleaner | 8 | true |
Neben dem main
-Thread, der die Anweisungen der öffentlichen static
-Methode main
ausführt, gibt es bei OpenJDK 16 noch die folgenden “technischen” Threads:
- Garbage Collection
Reference Handler
: Dies ist eine native (also nicht in Java implementierte) Funktion der VM. Sie ist nicht im Java-Sprachumfang spezifiziert, sondern aus technischen Gründen der VM vorhanden. Ihre Funktion ist es, Finalizer (s.u.) in einer Warteschlange zu verwalten und denFinalizer
- bzw. denCommon Cleaner
-Thread zu aktivieren, um die jeweiligefinalize
-Methode auszuführen.Finalizer
: Dieser Thread holt Objekte aus der Finalizer-Warteschlange und ruft diefinalize
-Methode (geerbt von WurzelklasseObject
) auf.Common Cleaner
: Ist eine native (also nicht in Java implementierte) alternative Realisierung vonFinalizer
. Durch die native Umsetzung ist sie besonders effizient und leichtgewichtig. Um nicht zu riskieren denReference Handler
Thread zu blockieren wird empfohlen nur sehr einfachefinalize
-Methoden mit einem Cleaner zu finalisieren. In allen anderen Fällen wird derReference Handler
einenFinalizer
verwenden.
Signal Dispatcher
: Dieser Thread hat die Aufgabe, Signale von der Ebene des Betriebssystems als Ablaufumgebung der Java Virtual Machine an andere Komponenten der JVM zu verteilen.Notification Thread
: unklar (mglw. im Zusammenhang mitwait()
/notify()
)
1.2 Start eines Threads in Java
In Java gibt es grundsätzlich zwei verschiedene Möglichkeiten nebenläufige Programmteile zu erhalten:
- Man leitet direkt von der Klasse
Thread
ab und überschreibt dierun
-Methode. - Man stellt eine Klasse bereit, die das
Runnable
-Interface implementiert. Ein Objekt dieser Klasse wird auch oft als Task bezeichnet. Es wird dann einemThread
-Objekt zur Ausführung übergeben.
Die so vorbereitete Thread
-Instanz wird durch Aufruf der Methode start()
zur nebenläufigen Abarbeitung markiert. Der Java-Scheduler sorgt dafür, dass Kontextwechsel zu diesem Thread erfolgen bzw. dass er in einer Execution-Engine (“Prozessor”) abgearbeitet wird. Wann Wechsel zu diesem Thread erfolgen, liegt aber in der Verantwortung des Schedulers, der eine eigene Strategie verfolgen kann. Die Priority Eigenschaft des Threads sollte dabei berücksichtigt werden.
1.2.1 Sequenzdiagramm für das Erzeugen von Threads
- Erzeugen des “main Thread”
- Das Betriebssystem startet die Java Virtual Machine (JVM).
- Die JVM lädt die zu startende Klasse (hier
Main.class
) mit einem System Call und - startet die
main
-Methode in einem eigenen Thread (“main Thread”) hier als grüne Lifeline dargestellt.
- Erzeugen des Threads
t
- Aus dem “main Thread” (grüne Lifeline) wird ein neues
Thread
-Objekt instanziiert. - An diesem Objekt wird die Methode
start()
aus dem “main Thread” heraus aufgerufen. start()
bewirkt, dass die JVM einen neuen Thread bereitstellt und dierun()
Methode vont
darin ausführt.- Die
run()
Methode wird nebenläufig zum “main Thread” ausgeführt (blaue Lifeline).
- Aus dem “main Thread” (grüne Lifeline) wird ein neues
1.2.2 Thread
-Konstruktor und Runnable
public Thread(Runnable target)
public Thread(Runnable target, String name)
Dem Konstruktor von Thread
kann man ein Objekt als Parameter übergeben, das das Runnable
-Interface implementiert. Optional kann auch ein String
für die name
-Eigenschaft mit übergeben werden.
1.2.3 Starten eines neuen Threads mithilfe eines Runnable
-Objekts
- Erzeugen des Threads
t
- Aus dem “main Thread” (grüne Lifeline) wird ein neues
Thread
-Objekt instanziiert. Dabei wird der Konstruktor verwendet, der ein “Runnable
-Objekt” als ersten Parameter hat. - An diesem Objekt wird die Methode
start()
aus dem “main Thread” heraus aufgerufen. start()
bewirkt, dass die JVM einen neuen Thread bereitstellt, und dierun()
Methode vont
darin ausführt.- Die
run()
Methode vont
besteht daraus, dass dierun()
Methode des “Runnable
-Objekts” ausgeführt wird. - Die
run()
Methoden werden nebenläufig zum “main Thread” ausgeführt (blaue Lifelines).
- Aus dem “main Thread” (grüne Lifeline) wird ein neues
Die Notation r: Runnable
für das “Runnable
-Objekt” ist nicht ganz korrekt: Runnable
ist ein Interface, von dem es keine Instanzen geben kann. Stattdessen gibt es eine Klasse X
, die das Interface Runnable
implementiert. r
müsste dann eine Instanz von X
sein: r: X implements Runnable
.
1.2.4 Runnable
als anonyme innere Klasse
1.2.5 Runnable
als Lambda-Ausdruck
Da der erste Parameter des Konstruktors von Thread
vom Typ Runnable
sein muss und Runnable
nur genau eine Methode hat, die überschrieben werden kann, kann anstelle der anonymen inneren Klasse auch ein Lambda-Ausdruck verwendet werden. Das, was vor dem “Pfeil” (->
) steht, ist die Parameterliste der Implementierung der einzigen abstrakten Methode des Parameter-Typs. Das, was nach dem Pfeil kommt, wird als Rumpf der überschriebenen Methode verwendet.
Es gibt dabei diverse abkürzende Notationen. Ein Beispiel ist:
- Wenn der Rumpf nur aus einem
return
-Statement besteht, können die geschweiften Klammern und das Schlüsselwortreturn
entfallen.
1.3 Lebenszyklus von Java Threads
1.3.1 Warten auf das Ende eines anderen Threads
void join()
void join(long millis)
void join(long millis, int nanos)
Die Varianten mit Parameter warten für eine gewisse Zeit. Sollte der Thread, auf dessen Ende gewartet wird, bis dahin nicht beendet sein, geht es trotzdem weiter.
millis == 0
bedeutet “für immer” (genau wie join()
)
1.3.2 Vereinfachter Lebenszyklus eines Thread
-Objekts
abgewandelt aus Hettel und Tran (2016, 19)
(*) Bedingungen: s. nächste Folie
- Nachdem eine neue Instanz von
Thread
oder einer Subklasse vonThread
erzeugt wurde, befindet sich das Objekt (this
) im ZustandNEW
. DasThread
-Objekt “läuft” aber noch nicht (nebenläufig). Deshalb liefert
this.isAlive() == false
. - Wird an dem
Thread
-Objekt, das sich im ZustandNEW
befindet, die Methodestart()
aufgerufen, wird die Methoderun()
des Objekts nebenläufig ausgeführt. Der Thread ist nun im ZustandRUNNABLE
.
this.isAlive() == true
. - Irgendwann endet die nebenläufige Abarbeitung von
run()
.
(*) Die Bedingungen dazu sind auf der folgenden Folie beschrieben.
Nachdemrun()
beendet ist, ist der Thread im ZustandTERMINATED
. Wieder gilt:this.isAlive() == false
.
Es ist nicht möglich, einen Thread aus dem TERMINATED
-Zustand zu reaktivieren (z.B. durch erneuten Aufruf von start()
).
1.3.3 Thread
-Ende
- reguläres Ende der
run()
-Methode - nicht behandelte Exception in
run()
- ein anderer Thread ruft
stop()
am zu beendenden Thread auf (deprecated ebenso wiepause()
undresume()
!) - hat Daemon-Eigenschaft (
Thread.currentThread().setDaemon(true)
) und alle User-Threads sind beendet System.exit()
wird in irgendeinem Thread der JVM aufgerufen
1.3.4 Exkurs: “Zombie”-Zustand von Prozessen unter UNIX/Linux (nicht prüfungsrelevant)
Im Prozess-Lebenszyklus bei UNIX gibt es einen Zustand, in dem ein Prozess “fast” beendet ist. Er benötigt keine Rechenressourcen mehr und fast sein ganzer Speicher ist bereits wieder freigegeben. Lediglich ein kurzer Block mit Prozessinformationen existiert noch im RAM. Dieser Zustand wird immer durchlaufen, wenn ein Prozess endet. In der Regel wird er aber sehr schnell wieder verlassen. Dies geschieht, indem ein anderer Prozess (in der Regel der Elternprozess, z.B. eine Shell) das Resultat (der Exit-Status, bzw. der Wert, der beim Systemcall exit
als Parameter übergeben wurde) des endenden Prozesses durch Aufruf des Systemcalls wait
ausliest. Dann wird der Zombie-Zustand verlassen und auch der Rest des Prozesses freigegeben (“reaping”). Die Prozess-ID des nun tatsächlich beendeten Prozesses ist wieder frei und kann neu vergeben werden.
Die Benennung “Zombie” rührt daher, dass solch ein Prozess nicht mehr lebendig ist (kann nicht mehr rechnen oder anders arbeiten), aber auch noch nicht ganz tot, da er einen Eintrag in der Prozesstabelle hat.
Das folgende C-Programm zombie.c
erzeugt mit fork
einen Kindprozess, der kurz darauf terminiert (durch exit(0)
). Im Elternprozess wird jedoch nicht wait
aufgerufen, sondern stattdessen etwas anderes getan (schlafen für 1 Minute mit sleep(60)
). Solange der Elternprozess lebt, bleibt der Kindprozess im Zombie-Zustand.
#include <stdlib.h>
#include <unistd.h>
int main() {
if (fork() == 0) {
exit(0);
} else {
sleep(60);
}
}
Unter Linux können Sie mit ps -ax
die Liste aller laufenden Prozesse zusammen mit Informationen u.a. zum Status ausgeben lassen. Der Zombie-Zustand wird in dieser Liste mit einem Wert von Z
in der Spalte STAT
gekennzeichnet.
Das Programm zombie
(der mit cc -o zombie zombie.c
compilierte C-Code von oben) wird im Hintergrund gestartet und danach die Liste der Prozesse ausgegeben.
15340 pts/1 SN 0:00 ./zombie
15342 pts/1 S+ 0:00 grep zombie
15343 pts/1 ZN 0:00 [zombie] <defunct>
Der Elternprozess ist in diesem Beispiel der Prozess mit der ID 15340
, der bereits terminierte aber noch als Zombie vorhandene Kindprozess hat die ID 15343
.
Ruft man nach mehr als 60 Sekunden erneut ps -ax | grep zombie
auf, ist der Elternprozess inzwischen terminiert (er wurde durch die Shell gestartet, die korrekt mit wait den Eintrag 15340
in der Prozess-Table gelöscht hat), was auch alle Kindprozesse, speziell den Zombie, gelöscht hat.
15927 pts/1 S+ 0:00 grep zombie
1.3.5 Thread
-Stoppen durch Signalisieren mit Variable
Hier ist es das Ziel, einen laufenden Thread “von außen” zu beenden: Ein anderer Thread entscheidet, dass es Zeit ist, einen Thread zu beenden. Da die Methode stop()
von Thread deprecated ist (s. Abschnitt “Thread-Ende”), muss etwas anderes gemacht werden. Das Folgende ist die “idomatische” Lösung in Java. Dabei wird eine Variable verwendet, die über eine “Stopper-Methode” (stopRequest()
) nach außen sichtbar gemacht wird. Hier passiert im Einzelnen Folgendes:
public class Main {
1 public static void main(String... args)
throws InterruptedException {
2 var task = new Task();
4 var thread = new Thread(task);
5 thread.start();
7 Thread.sleep(4000);
8 task.stopRequest();
thread.join();
}
}
public class Task implements Runnable {
3 private volatile boolean stopped;
private volatile Thread self;
public boolean isStopped() {
return this.stopped;
}
public void stopRequest() {
9 this.stopped = true;
if (this.self != null) {
10 this.self.interrupt();
}
}
@Override
6 public void run() {
this.self = Thread.currentThread();
while (!isStopped()) {
// ... arbeiten ...
}
// ... aufräumen ...
}
}
- 1
-
Das Programm wird gestartet, indem die
main
-Methode aufgerufen wird. Es beginnt die Abarbeitung im “main Thread”: - 2
-
Eine neue Instanz der Klasse
Task
wird erzeugt. Das ist einRunnable
, also ein Objekt einer Klasse, die die Methoderun()
implementiert. - 3
-
Die Instanzvariablen von
Task
werden mitfalse
bzw.null
initialisiert. Das Schlüsselwortvolatile
ist erforderlich, es wird im nächsten Kapitel erklärt. Im Moment bitte einfach hinnehmen. Wenn Sie es weglassen, dürfte es nicht funktionieren. - 4
-
Eine neue Instanz von Thread wird erzeugt, wobei das
Runnable
task
als Parameter übergeben wird. Eine Referenz auf diesen neuen Thread wird in der lokalen Variablenthread
gespeichert. Dieser Thread ist im ZustandNEW
, er läuft also noch nicht nebenläufig (thread.isAlive == false
). - 5
-
Der Thread wird gestartet, d.h. ab jetzt wird die Methode
run()
des Objektstask
nebenläufig ausgeführt. Dieser Thread ist nun im ZustandRUNNABLE
, er läuft jetzt also nebenläufig (thread.isAlive == true
). - 6
-
Im neuen Thread wird nun nebenläufig zum “main Thread” die
run()
-Methode abgearbeitet. - 7
-
Nun wird der “main Thread” für 4000 ms zum Schlafen geschickt. Er ist in dieser Zeit inaktiv (braucht keine Ressourcen). Der andere Thread (Referenz in lokaler Variablen
thread
) läuft währenddessen weiter (Abarbeitung der Methodetask.run()
). - 8
-
Irgendwann später geht es im “main Thread” weiter: Der “main Thread” ruft die Methode
stopRequest()
antask
auf, um den nebenläufigen Thread (Referenz in lokaler Variablethread
) zu beenden. - 9
-
Der nebenläufige Thread (Referenz in lokaler Variable
thread
) wird dadurch beeinflusst, dass die Instanzvariablestopped
, die der nebenläufige Thread in derwhile
-Schleife seinerrun()
-Methode durch Aufruf vonisStopped()
ständig kontrolliert, auftrue
gesetzt wird. - 10
-
Außerdem muss der nebenläufige Thread (Referenz in lokaler Variable
thread
) auch noch mitinterrupt()
unterbrochen werden, falls im Schleifenrumpf derrun()
-Methode einsleep()
oder etwas vergleichbares abgebrochen werden muss. Sollte diestopThread()
-Methode sehr früh im Lebenszyklus des Threads aufgerufen werden (bevor dierun()
-Methode gestartet wurde oder solangeself
in derrun()
-Methode noch nicht gesetzt ist), ist die Instanzvariableself
nochnull
(von der Initialisierung). In dem Fall darfinterrupt()
nicht aufgerufen werden. Deshalb ist die Fallunterscheidung aufnull
um den Aufruf voninterrupt()
herum. Es kann auch nicht sein, dass der Thread schon läuft und in einemsleep
oder einer anderen zu unterbrechenden Aktion steckt, da zu diesem Zeitpunkt dierun()
-Methode noch nicht in den Schleifenrumpf gekommen sein kann.
1.3.6 Thread
-Stoppen durch Signalisieren mit Variable (Alternative)
public class Main {
public static void main(String... args) //
throws InterruptedException {
var thread = new Task();
thread.start();
Thread.sleep(4000);
thread.stopRequest();
thread.join();
}
}
Alternativ: Hier erbt der nebenläufige Task von Thread
, statt dem Thread
-Konstruktor im “main Thread” ein Runnable
als Parameter zu übergeben.
public class Task extends Thread {
private volatile boolean stopped;
private volatile Thread self;
public boolean isStopped() {
return this.stopped;
}
public void stopRequest() {
this.stopped = true;
if (this.self != null) {
this.self.interrupt();
}
}
@Override
public void run() {
this.self = Thread.currentThread();
while (!isStopped()) {
// ... arbeiten ...
}
// ... aufräumen ...
}
}
In diesem Fall kennt der Thread sich selbst und kann damit den Interrupt anders umsetzen:
public class Task extends Thread {
private volatile boolean stopped;
public boolean isStopped() {
return this.stopped;
}
public void stopRequest() {
this.stopped = true;
if (this.isAlive()) {
this.interrupt();
}
}
@Override
public void run() {
while (!isStopped()) {
// ... arbeiten ...
}
// ... aufräumen ...
}
}
1.3.7 Lebenszyklus (Zustände) eines Thread
-Objekts
BLOCKED
: mehr dazu kommt nächste WocheWAITING
: mehr dazu kommt übernächste WocheTIMED_WAITING
: wartet z.B. wg.Thread.sleep(...)
. Durchinterrupt()
kann der Thread zurück zuRUNNABLE
wechseln.
abgewandelt aus Hettel und Tran (2016, 26)
Für den grundlegenden Ablauf (außerhalb RUNNABLE
) s. Abschnitt “Vereinfachter Lebenszyklus eines Thread
-Objekts”.
Innerhalb des unbenannten verschachtelter Zustand passiert folgendes: Der Thread kommt in den Zustand RUNNABLE
und führt nebenläufig die Methode run()
aus: Anweisungen aus der run()
-Methode werden Schritt für Schritt ausgeführt.
BLOCKED
: Ein Thread versucht, ein Objekt zu sperren, kann aber die Sperre nicht erlangen. Er wird dadurch blockiert und kann auch nicht über eineninterrupt()
-Aufruf aus der Blockade geholt werden. Threads, die z.B. an einer mitsynchronized
gekennzeichneten Methode warten, befinden sich imBLOCKED
-Zustand.WAITING
: Ein Thread hat eine Wartemethode (wait()
,await()
) aufgerufen und bleibt in diesem Zustand, bis ein bestimmtes Ereignis eintritt (Beispieljoin()
). Danach wechselt er wieder in den Zustand…TIMED_WAINITNG
: Hier wartet ein lauffähiger Thread nur für eine bestimmte Zeitspanne auf ein Ereignis. Typische Methoden sind z.B. die überladenen Versionen vonjoin()
. Analog zumWAITING
-Zustand kann ein Thread durchinterrupt()
aus der Blockade gelöst werden.
Der Thread bleibt die ganze Zeit “lebendig”, in der er im Zustand RUNNABLE
ist. Also auch, wenn gerade keine Anweisungen aus der run()
-Methode ausgeführt werden, weil er BLOCKED
, WAITING
oder im Zustand TIMED_WAITING
ist.