Einführung in die nebenläufige Programmierung mit Threads
Threads sind weitere Kontrollflussfäden, die von der Java-VM (oder (selten) vom OS) verwaltet werden. Damit ist sind sie leichtgewichtiger als der Start neuer Prozesse direkt auf Betriebssystem-Ebene.
Beim Start eines Java-Programms wird die main()
-Methode automatisch in einem
(Haupt-) Thread ausgeführt. Alle Anweisungen in einem Thread werden sequentiell
ausgeführt.
Um einen neuen Thread zu erzeugen, leitet man von Thread
ab oder implementiert
das Interface Runnable
. Von diesen eigenen Klassen kann man wie üblich ein neues
Objekt anlegen. Die Methode run()
enthält dabei den im Thread auszuführenden
Code. Um einen Thread als neuen parallelen Kontrollfluss zu starten, muss man die
geerbte Methode start()
auf dem Objekt aufrufen. Im Fall der Implementierung von
Runnable
muss man das Objekt zuvor noch in den Konstruktor von Thread
stecken
und so ein neues Thread
-Objekt erzeugen, auf dem man dann start()
aufrufen kann.
Threads haben einen Lebenszyklus: Nach dem Erzeugen der Objekte mit new
wird
der Thread noch nicht ausgeführt. Durch den Aufruf der Methode start()
gelangt
der Thread in einen Zustand "ausführungsbereit". Sobald er vom Scheduler eine
Zeitscheibe zugeteilt bekommt, wechselt er in den Zustand "rechnend". Von hier kann
er nach Ablauf der Zeitscheibe durch den Scheduler wieder nach "ausführungsbereit"
zurück überführt werden. Dieses Wechselspiel passiert automatisch und i.d.R. schnell,
so dass selbst auf Maschinen mit nur einem Prozessor/Kern der Eindruck einer parallelen
Verarbeitung entsteht. Nach Abarbeitung der run()
-Methode wird der Thread beendet
und kann nicht wieder neu gestartet werden. Bei Zugriff auf gesperrte Ressourcen
oder durch sleep()
oder join()
kann ein Thread blockiert werden. Aus diesem
Zustand gelangt er durch Interrupts oder nach Ablauf der Schlafzeit oder durch notify
wieder zurück nach "ausführungsbereit".
Die Thread-Objekte sind normale Java-Objekte. Man kann hier Attribute und Methoden haben und diese entsprechend zugreifen/aufrufen. Das klappt auch, wenn der Thread noch nicht gestartet wurde oder bereits abgearbeitet wurde.
- (K2) Grundsätzlicher Unterschied zw. Threads und Prozessen
- (K2) Lebenszyklus von Threads
- (K3) Erzeugen und Starten von Threads
- (K3) Kommunikation mit Objekten
42
Einführung in nebenläufige Programmierung
Traditionelle Programmierung
- Aufruf einer Methode verlagert Kontrollfluss in diese Methode
- Code hinter Methodenaufruf wird erst nach Beendigung der Methode ausgeführt
public class Traditional {
public static void main(String... args) {
Traditional x = new Traditional();
System.out.println("main(): vor run()");
x.run();
System.out.println("main(): nach run()");
}
public void run() {
IntStream.range(0, 10).mapToObj(i -> "in run()").forEach(System.out::println);
}
}
Nebenläufige Programmierung
- Erzeugung eines neuen Kontrollflussfadens (Thread)
- Läuft (quasi-) parallel zu bisherigem Kontrollfluss
- Threads können unabhängig von einander arbeiten
- Zustandsverwaltung durch Java-VM (oder Unterstützung durch Betriebssystem)
- Aufruf einer bestimmten Methode erzeugt neuen Kontrollflussfaden
- Der neue Thread arbeitet "parallel" zum bisherigen Thread
- Kontrolle kehrt sofort wieder zurück: Code hinter dem Methodenaufruf wird ausgeführt ohne auf die Beendigung der aufgerufenen Methode zu warten
- Verteilung der Threads auf die vorhandenen Prozessorkerne abhängig von der Java-VM
public class Threaded extends Thread {
public static void main(String... args) {
Threaded x = new Threaded();
System.out.println("main(): vor run()");
x.start();
System.out.println("main(): nach run()");
}
@Override
public void run() {
IntStream.range(0, 10).mapToObj(i -> "in run()").forEach(System.out::println);
}
}
Erzeugen von Threads
-
Ableiten von
Thread
oder Implementierung vonRunnable
-
Methode
run()
implementieren, aber nicht aufrufen -
Methode
start()
aufrufen, aber (i.d.R.) nicht implementieren
Ableiten von Thread
start()
startet den Thread und sorgt für Ausführung vonrun()
start()
nur einmal aufrufen
Implementierung von Runnable
- Ebenfalls
run()
implementieren - Neues
Thread
-Objekt erzeugen, Konstruktor das eigene Runnable übergeben - Für Thread-Objekt die Methode
start()
aufrufen- Startet den Thread (das Runnable) und sorgt für Ausführung von
run()
- Startet den Thread (das Runnable) und sorgt für Ausführung von
Vorteil von Runnable
: Ist ein Interface, d.h. man kann noch von einer anderen Klasse erben
Zustandsmodell von Threads (vereinfacht)
Threads haben einen Lebenszyklus: Nach dem Erzeugen der Objekte mit new
wird
der Thread noch nicht ausgeführt. Er ist sozusagen in einem Zustand "erzeugt".
Man kann bereits mit dem Objekt interagieren, also auf Attribute zugreifen und
Methoden aufrufen.
Durch den Aufruf der Methode start()
gelangt der Thread in einen Zustand
"ausführungsbereit", er läuft also aus Nutzersicht. Allerdings hat er noch keine
Ressourcen zugeteilt (CPU, ...), so dass er tatsächlich noch nicht rechnet. Sobald
er vom Scheduler eine Zeitscheibe zugeteilt bekommt, wechselt er in den Zustand
"rechnend" und führt den Inhalt der run()
-Methode aus. Von hier kann er nach
Ablauf der Zeitscheibe durch den Scheduler wieder nach "ausführungsbereit" zurück
überführt werden. Dieses Wechselspiel passiert automatisch und i.d.R. schnell,
so dass selbst auf Maschinen mit nur einem Prozessor/Kern der Eindruck einer parallelen
Verarbeitung entsteht.
Nach der Abarbeitung der run()
-Methode oder bei einer nicht gefangenen Exception
wird der Thread beendet und kann nicht wieder neu gestartet werden. Auch wenn der
Thread abgelaufen ist, kann man mit dem Objekt wie üblich interagieren (nur eben
nicht mehr parallel).
Bei Zugriff auf gesperrte Ressourcen oder durch Aufrufe von Methoden wie sleep()
oder join()
kann ein Thread blockiert werden. Hier führt der Thread nichts aus,
bekommt durch den Scheduler aber auch keine neue Zeitscheibe zugewiesen. Aus diesem
Zustand gelangt der Thread wieder heraus, etwa durch Interrupts (Aufruf der Methode
interrupt()
auf dem Thread-Objekt) oder nach Ablauf der Schlafzeit (in sleep()
)
oder durch ein notify
, und wird wieder zurück nach "ausführungsbereit" versetzt
und wartet auf die Zuteilung einer Zeitscheibe durch den Scheduler.
Sie finden in [Boles2008, Kapitel 5.2 "Thread-Zustände"] eine schöne ausführliche Darstellung.
Threads können wie normale Objekte kommunizieren
- Zugriff auf (
public
) Attribute (oder eben über Methoden) - Aufruf von Methoden
Threads können noch mehr
-
Eine Zeitlang schlafen:
Thread.sleep(<duration_ms>)
- Statische Methode der Klasse
Thread
(Klassenmethode) - Aufrufender Thread wird bis zum Ablauf der Zeit oder bis zum Aufruf
der
interrupt()
-Methode des Threads blockiert - "Moderne" Alternative:
TimeUnit
, beispielsweiseTimeUnit.SECONDS.sleep( 2 );
- Statische Methode der Klasse
-
Prozessor abgeben und hinten in Warteschlange einreihen:
yield()
-
Andere Threads stören:
otherThreadObj.interrupt()
- Die Methoden
sleep()
,wait()
undjoin()
im empfangenden ThreadotherThreadObj
lösen eineInterruptedException
aus, wenn sie durch die Methodeinterrupt()
unterbrochen werden. Das heißt,interrupt()
beendet diese Methoden mit der Ausnahme. - Empfangender Thread verlässt ggf. den Zustand "blockiert" und wechselt in den Zustand "ausführungsbereit"
- Die Methoden
-
Warten auf das Ende anderer Threads:
otherThreadObj.join()
- Ausführender Thread wird blockiert (also nicht
otherThreadObj
!) - Blockade des Aufrufers wird beendet, wenn der andere Thread
(
otherThreadObj
) beendet wird.
- Ausführender Thread wird blockiert (also nicht
Hinweis: Ein Thread wird beendet, wenn
- die
run()
-Methode normal endet, oder - die
run()
-Methode durch eine nicht gefangene Exception beendet wird, oder - von außen die Methode
stop()
aufgerufen wird (Achtung: Deprecated! Einen richtigen Ersatz gibt es aber auch nicht.).
Hinweis: Die Methoden wait()
, notify()
/notifyAll()
und die "synchronized
-Sperre"
werden in der Sitzung ["Threads: Synchronisation"](threads-intro.
besprochen.
Wrap-Up
Threads sind weitere Kontrollflussfäden, von Java-VM (oder (selten) von OS) verwaltet
- Ableiten von
Thread
oder implementieren vonRunnable
- Methode
run
enthält den auszuführenden Code - Starten des Threads mit
start
(nie mitrun
!)
- [Boles2008] Parallele Programmierung spielend gelernt mit dem Java-Hamster-Modell
Boles, D., Vieweg+Teubne, 2008. ISBN 978-3-8351-0229-3. - [Java-SE-Tutorial] The Java Tutorials
Oracle Corporation, 2022.
Trail: Essential Java Classes, Lesson: Concurrency - [Ullenboom2021] Java ist auch eine Insel
Ullenboom, C., Rheinwerk-Verlag, 2021. ISBN 978-3-8362-8745-6.
Kap. 16: Einführung in die nebenläufige Programmierung