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: Die Id ist eine Zahl. In einer Java VM Instanz wird die Id jeweils nur einmal vergeben. Jeder Thread hat also eine andere Id.
  • Name: Jeder Thread hat neben der eindeutigen numerischen Id eine textuelle Repräsentation. Wird der Name nicht individuell gesetzt, wird nach einem festen Muster ein textueller Name aus der Id gebildet.
  • Priority: Jedes Thread-Objekt besitzt eine Priorität. Die Priorität entspricht einer Zahl zwischen 1 (Thread.MIN_PRIORITY) und 10 (Thread.MAX_PRIORITY) und kann mit setPriority gesetzt und mit getPriority 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

Hettel und Tran (2016, 13)

Hettel und Tran (2016, 13)

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, &param);

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:

pthread_create(&tid, &attr, func, arg);

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:

import org.apache.commons.lang3.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:

  1. Wenn ThreadUtils.getAllThreads() in einem beliebigen Thread aufgerufen wird, holt die Methode sich mit Thread.currentThread() eine Referenz auf diesen Thread.
  2. Von diesem Thread wird dessen Threadgruppe mit getThreadGroup() erfragt.
  3. Von dieser ThreadGroup wird solange die Eltern-Threadgruppe mit getParent() ermittelt, bis es keine Eltern-Threadgruppe mehr gibt, man also die Wurzel-Threadgruppe (“System Threadgruppe”) ermittelt hat.
  4. An der Wurzel-Threadgruppe wird dann die Methode enumerate(list, true)9 aufgerufen. Sie kopiert alle aktiven Threads in list (vom Typ Thread[]). Der zweite Parameter true bewirkt, dass alle Untergruppen der Wurzel-Threadgruppe rekursiv auf dieselbe Art behandelt werden. Am Ende sind also alle aktiven Threads in list enthalten.
  5. Zum Schluss wird dieses Thread-Array in eine List<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.

/* ... (u.a. printFeatures) ... */
for (var t : ThreadUtils.getAllThreads()) {
    printFeatures(t.threadId(), t.getName(), t.getPriority(), t.isDaemon());
}

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 den Finalizer- bzw. den Common Cleaner-Thread zu aktivieren, um die jeweilige finalize-Methode auszuführen.
    • Finalizer: Dieser Thread holt Objekte aus der Finalizer-Warteschlange und ruft die finalize-Methode (geerbt von Wurzelklasse Object) auf.
    • Common Cleaner: Ist eine native (also nicht in Java implementierte) alternative Realisierung von Finalizer. Durch die native Umsetzung ist sie besonders effizient und leichtgewichtig. Um nicht zu riskieren den Reference Handler Thread zu blockieren wird empfohlen nur sehr einfache finalize-Methoden mit einem Cleaner zu finalisieren. In allen anderen Fällen wird der Reference Handler einen Finalizer 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 mit wait()/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 die run-Methode.
  • Man stellt eine Klasse bereit, die das Runnable-Interface implementiert. Ein Objekt dieser Klasse wird auch oft als Task bezeichnet. Es wird dann einem Thread-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 die run() Methode von t darin ausführt.
    • Die run() Methode wird nebenläufig zum “main Thread” ausgeführt (blaue Lifeline).

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.

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

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 die run() Methode von t darin ausführt.
    • Die run() Methode von t besteht daraus, dass die run() Methode des “Runnable-Objekts” ausgeführt wird.
    • Die run() Methoden werden nebenläufig zum “main Thread” ausgeführt (blaue Lifelines).

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

new Thread(new Runnable()   {

    @Override
    public void run() {
        /*...*/
    }
});

1.2.5 Runnable als Lambda-Ausdruck

new Thread(            () ->


                      {
        /*...*/
    }
 );

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üsselwort return entfallen.
new Thread(() -> {/*...*/});

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

  1. Nachdem eine neue Instanz von Thread oder einer Subklasse von Thread erzeugt wurde, befindet sich das Objekt (this) im Zustand NEW. Das Thread-Objekt “läuft” aber noch nicht (nebenläufig). Deshalb liefert
    this.isAlive() == false.
  2. Wird an dem Thread-Objekt, das sich im Zustand NEW befindet, die Methode start() aufgerufen, wird die Methode run() des Objekts nebenläufig ausgeführt. Der Thread ist nun im Zustand RUNNABLE.
    this.isAlive() == true.
  3. Irgendwann endet die nebenläufige Abarbeitung von run().
    (*) Die Bedingungen dazu sind auf der folgenden Folie beschrieben.
    Nachdem run() beendet ist, ist der Thread im Zustand TERMINATED. 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 wie pause() und resume()!)
  • 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.

./zombie & ps -ax | grep zombie

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 ein Runnable, also ein Objekt einer Klasse, die die Methode run() implementiert.
3
Die Instanzvariablen von Task werden mit false bzw. null initialisiert. Das Schlüsselwort volatile 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 Variablen thread gespeichert. Dieser Thread ist im Zustand NEW, 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 Objekts task nebenläufig ausgeführt. Dieser Thread ist nun im Zustand RUNNABLE, 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 Methode task.run()).
8
Irgendwann später geht es im “main Thread” weiter: Der “main Thread” ruft die Methode stopRequest() an task auf, um den nebenläufigen Thread (Referenz in lokaler Variable thread) zu beenden.
9
Der nebenläufige Thread (Referenz in lokaler Variable thread) wird dadurch beeinflusst, dass die Instanzvariable stopped, die der nebenläufige Thread in der while-Schleife seiner run()-Methode durch Aufruf von isStopped() ständig kontrolliert, auf true gesetzt wird.
10
Außerdem muss der nebenläufige Thread (Referenz in lokaler Variable thread) auch noch mit interrupt() unterbrochen werden, falls im Schleifenrumpf der run()-Methode ein sleep() oder etwas vergleichbares abgebrochen werden muss. Sollte die stopThread()-Methode sehr früh im Lebenszyklus des Threads aufgerufen werden (bevor die run()-Methode gestartet wurde oder solange self in der run()-Methode noch nicht gesetzt ist), ist die Instanzvariable self noch null (von der Initialisierung). In dem Fall darf interrupt() nicht aufgerufen werden. Deshalb ist die Fallunterscheidung auf null um den Aufruf von interrupt() herum. Es kann auch nicht sein, dass der Thread schon läuft und in einem sleep oder einer anderen zu unterbrechenden Aktion steckt, da zu diesem Zeitpunkt die run()-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 Woche
  • WAITING: mehr dazu kommt übernächste Woche
  • TIMED_WAITING: wartet z.B. wg. Thread.sleep(...). Durch interrupt() kann der Thread zurück zu RUNNABLE 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 einen interrupt()-Aufruf aus der Blockade geholt werden. Threads, die z.B. an einer mit synchronized gekennzeichneten Methode warten, befinden sich im BLOCKED-Zustand.
  • WAITING: Ein Thread hat eine Wartemethode (wait(), await()) aufgerufen und bleibt in diesem Zustand, bis ein bestimmtes Ereignis eintritt (Beispiel join()). 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 von join(). Analog zum WAITING-Zustand kann ein Thread durch interrupt() 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.