Multithreading 🧵
👉 Siehe Definitionen für Multitasking, Multiprocessing und Multithreading!
⚠️ Diese Seite deckt (ganz bewusst) bei weitem nicht alles ab, was es zu Multithreading in Java zu sagen gäbe! Zu diesem Thema wurden ganze Bücher geschrieben. Hier wird nur sehr oberflächlich das allgemeine Konzept mit den dazugehörigen Sprach-Features beschrieben. Die im Text verteilten Verweise auf externe Ressourcen bieten weitere Informationen zum Thema.
🔗 Für einen tieferen Einstieg in das Thema können außerdem dieser, dieser, oder dieser Link nützlich sein!
Ein Java-Programm läuft in einem eigenen Thread (in diesem wird die main
-Methode ausgeführt). Durch das Ausführen von Teilen des Programmes in zusätzlich erzeugten Threads lassen sich Vorgänge parallelisieren (dieses Konzept wird Concurrency genannt).
In Java gibt es mehrere Möglichkeiten einen neuen Thread (auch: leichtgewichtiger Prozess oder sub-process) zu erzeugen. Die in den meisten Fällen “richtige” ist das Implementieren des Interfaces Runnable
(siehe unten).
💬 Man kann auch die Klasse
Thread
erweitern und dierun()
-Methode überschreiben, allerdings ergibt dieses Vorgehen nur dann Sinn, wenn man wirklich das Verhalten vonThread
erweitern möchte - wenn es nur um das Erzeugen und Ausführen eines Code-Blocks in einem eigenen Thread geht, ist das Übergeben einesRunnable
s an eine Instanz vonThread
die semantisch und logistisch bessere Variante.
Die folgende Grafik illustriert das Verhältnis zwischen den Konzepten 👉 Prozess und 👉 Thread. Es werden hier zwei Threads gezeigt, die Teile eines Prozesses sind, der offensichtlich auf einer einzelnen 👉 CPU (mit einem Kern) ausgeführt wird, denn die Threads laufen nicht wirklich gleichzeitig, sondern wechseln sich ab:
Quelle: commons.wikimedia.org; I, Cburnett / CC BY-SA
Implementieren von Runnable
Das Implementieren des Interfaces Runnable
macht eine Klasse für einen Thread “ausführbar” - d.h. ein Thread kann den Code in der run()
-Methode parallel zum main-Thread (der die main
-Methode gestartet hat) ausführen.
Das folgende (lauffähige!) Beispiel demonstriert diesen Vorgang. Die statische Methode sleep()
dient hier lediglich als Wrapper zum Auffangen einer möglichen InterruptedException
, die auftreten kann, während der aufrufende Thread mit Thread.sleep()
um die angegebene Anzahl von Millisekunden pausiert wurde.
public class ImportantTask implements Runnable {
public static void main(String[] args) {
/*
* Instanz von Thread erzeugen und Instanz dieser Klasse
* (implementiert Runnable) dem Konstruktor übergeben
*/
Thread thread = new Thread(new ImportantTask());
// ausführen...
System.out.println("[MAIN] Starte neuen Thread ...");
thread.start(); // zusätzlichen Thread starten
System.out.println("[MAIN] Neuer Thread gestartet.");
ImportantTask.sleep(2000);
System.out.println("[MAIN] Status des zusätzlichen Threads: "
+ thread.getState());
}
@Override
public void run() {
System.out.println("[RUNNABLE] Ich leiste ...");
ImportantTask.sleep(1200);
System.out.println("[RUNNABLE] ... hier wirklich ...");
ImportantTask.sleep(1200);
System.out.println("[RUNNABLE] ... harte Arbeit!");
}
/*
* Diese Methode lässt den Thread, von dem aus
* sie aufgerufen wurde, um die übergebene
* Anzahl von Millisekunden pausieren.
*/
private static void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
System.err.println("Thread unterbrochen!");
}
}
}
Dieser Code erzeugt (nach und nach) folgende Ausgabe:
[MAIN] Starte neuen Thread ...
[MAIN] Neuer Thread gestartet.
[RUNNABLE] Ich leiste ...
[RUNNABLE] ... hier wirklich ...
[MAIN] Status des zusätzlichen Threads: TIMED_WAITING
[RUNNABLE] ... harte Arbeit!
Das Package java.util.concurrent
Das Package java.util.concurrent
enthält viele (!) Werkzeuge für die erfolgreiche Umsetzung von Multithreading. Weil der Umfang dieser Werkzeuge aus dem Rahmen dieser kurzen Einführung ins Thema fällt, sei an dieser Stelle auf Seiten wie diese, diese oder diese verwiesen.
Die folgenden Abschnitte zu synchronized
und volatile
umreißen kurz die Bedeutung zweier grundlegenderer Konzepte.
synchronized
Das Schlüsselwort synchronized
hat hauptsächlich die Aufgabe eine Methode oder Variable vor dem gleichzeitigen Zugriff durch mehrere Threads zu schützen. So lässt sich unvorhergesehenes Programm-Verhalten, das durch 👉 Race Conditions verursacht wird, verhindern.
Im folgenden Beispiel trägt die Methode printThreadNameFiveTimes()
, die in run()
aufgerufen wird, das Schlüsselwort synchrionized
:
public class ImportantTask implements Runnable {
public static void main(String[] args) {
// Instanz dieser Klasse (Runnable!) erzeugen
ImportantTask task = new ImportantTask();
// 3 Threads starten (mit dem selben Runnable)
new Thread(task, "EINS").start();
new Thread(task, "ZWEI").start();
new Thread(task, "DREI").start();
}
@Override
public void run() {
printThreadNameFiveTimes();
}
private synchronized void printThreadNameFiveTimes() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
try {
Thread.sleep(400);
} catch (InterruptedException e) {
System.err.println("Thread unterbrochen!");
}
}
}
}
Da synchronized
den Zugriff durch andere Threads so lange blockiert, bis die Methode im ersten aufrufenden Thread beendet ist, sieht die Ausgabe wie folgt aus:
EINS: 0
EINS: 1
EINS: 2
EINS: 3
EINS: 4
ZWEI: 0
ZWEI: 1
ZWEI: 2
ZWEI: 3
ZWEI: 4
DREI: 0
DREI: 1
DREI: 2
DREI: 3
DREI: 4
Entfernt man synchronized
nun aus der Signatur von printThreadNameFiveTimes()
, ändert sich die Ausgabe entsprechend. Die drei Threads greifen nun gleichzeitig auf die Methode zu:
EINS: 0
ZWEI: 0
DREI: 0
EINS: 1
DREI: 1
ZWEI: 1
EINS: 2
DREI: 2
ZWEI: 2
EINS: 3
DREI: 3
ZWEI: 3
EINS: 4
ZWEI: 4
DREI: 4
Eine Alternative zum synchronized
in der Methodensignatur ist das Umschließen des entsprechenden Codes mit einem synchronized
-Block. In den runden Klammern wird hier die Referenz auf die zu synchronisierende Ressource übergeben:
synchronized (this) {
printThreadNameFiveTimes();
}
volatile
💬 Dieser Abschnitt ist (und bleibt) nur eine kurze Beschreibung - vollständige Erläuterungen gibt es z.B. am Ende der untenstehenden Links!
Dieses Schlüsselwort ist speziell dazu gedacht, den Zugriff auf (Instanz- oder Klassen-) Variablen zu synchronisieren. Im Gegensatz zu synchronized
ist der Gleichzeitige Zugriff durch mehrere Threads dann möglich. Die Threads blockieren sich also nicht gegenseitig. Ist eine Variable als volatile
deklariert, müssen alle zugreifenden Threads ihre Werte für diese Variable aktualisieren, sobald die Variable geändert wird. Dieser Vorgang kostet natürlich seinerseits auch wieder Rechenleistung.