Java Threads

Parallele Programmierung (3IB)

Prof. Dr.-Ing. Sandro Leuchter
Hochschule Mannheim, Fakultät für Informatik
Wintersemester 2024/2025

 

Dieses Werk ist lizenziert unter einer Creative Commons „Namensnennung – Nicht-kommerziell – Weitergabe unter gleichen Bedingungen 4.0 International“ Lizenz.

Überblick

=> Grundlage: Hettel und Tran (2016, Kap. 2)

  • Nebenläufigkeit allgemein und Threads in Java
  • Threadnebenläufigkeit: Start eines Threads in Java
    • durch Ableiten von Thread
    • durch Implementieren von Runnable und im Thread-Konstruktor Übergeben
  • Lebenszyklus von Java Threads
    • Beenden von Threads
    • Warten auf das Ende anderer Threads
    • Lebenszyklus von Threads

Thread Nebenläufigkeit

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).

Grundlagen zum Java-Thread

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());
}

Die Methode availableProcessors und Hyper-Threading

  • Runtime.getRuntime().availableProcessors() \(\to\) 8 wegen Hyper-Threading, obwohl 1 CPU mit 4 Kernen

Zuordnung der Java-Threads zu einzelnen Kernen

Hettel und Tran (2016, 13)

Exkurs: “Technische” Threads der Java Virtual Machine1

import org.apache.commons.lang3.ThreadUtils;

/* ... (u.a. printFeatures) ... */
for (var t : ThreadUtils.getAllThreads()) {
    printFeatures(t.threadId(), t.getName(), t.getPriority(), t.isDaemon());
}
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

OpenJDK 16.0.2 64-Bit Server VM auf Intel Core i5-1145G7 @ 2.60GHz mit 16 GB RAM und x86_64 Linux (Kernel Version: 5.10.70)

Start eines Threads in Java

Start eines Threads in Java

  • 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.

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).

Laboraufgabe “Start eines Threads durch und bei Vererbung”

  • Projekt: pp.01.01-Inheritance
  • Bearbeitungszeit: 15 Minuten
  • Musterlösung: 15 Minuten
  • Kompatibilität: mindestens Java SE 10

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();
}

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).

Runnable als anonyme innere Klasse

new Thread(new Runnable()   {

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

Runnable als Lambda-Ausdruck

new Thread(            () ->


                      {
        /*...*/
    }
 );

Runnable als Lambda-Ausdruck (kurz)

new Thread(() -> {/*...*/});





Laboraufgabe “Start eines Threads als Runnable und Refactoring”

  • Projekt: pp.01.02-Runnable
  • Bearbeitungszeit: 10 Minuten
  • Musterlösung: 10 Minuten
  • Kompatibilität: mindestens Java SE 19

Lebenszyklus von Java Threads

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())

Vereinfachter Lebenszyklus eines Thread-Objekts

abgewandelt aus Hettel und Tran (2016, 19)

(*) Bedingungen: s. nächste Folie

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

Thread-Stoppen durch Signalisieren mit Variable

public class Main {
    public static void main(String... args) throws InterruptedException {
        var task = new Task();
        var thread = new Thread(task);
        thread.start();
        Thread.sleep(4000);
        task.stopRequest();
        thread.join();
    }
}

Thread-Stoppen durch Signalisieren mit Variable

public class Task implements Runnable {
    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 ...
    }
}

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.

Thread-Stoppen durch Signalisieren mit Variable (Alternative)

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 ...
    }

Thread-Stoppen durch Signalisieren mit Variable (Alternative)

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 ...
    }
}

Laboraufgabe “Lebenszyklus von Threads und Interrupts

  • Projekt: pp01.03-EndThread
  • Bearbeitungszeit: 20 Minuten
  • Musterlösung: 20 Minuten
  • Kompatibilität: mindestens Java SE 19

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)

Referenzen

Hettel, Jörg und Manh Tien Tran. 2016. Nebenläufige Programmierung mit Java. Konzepte und Programmiermodelle für Multicore-Systeme. Heildelberg: dpunkt.verlag.