archetype | title | linkTitle | author | readings | tldr | outcomes | quizzes | youtube | fhmedia | |||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
lecture-cg |
Einführung in die nebenläufige Programmierung mit Threads |
Intro |
Carsten Gips (HSBI) |
|
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.
|
|
|
|
[Demo: misc.SwingWorkerDemo (GUI ausprobieren)]{.bsp href="https://github.com/Programmiermethoden-CampusMinden/Prog2-Lecture/blob/master/lecture/threads/src/misc/SwingWorkerDemo.java"}
::: notes
:::
::: notes
::::
::: slides
:::
::: notes
- 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);
}
}
[Demo: intro.Traditional]{.bsp href="https://github.com/Programmiermethoden-CampusMinden/Prog2-Lecture/blob/master/lecture/threads/src/intro/Traditional.java"}
::: notes
::::
::: slides
:::
::: notes
- 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)]{.notes}
- 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);
}
}
[Demo: intro.Threaded]{.bsp href="https://github.com/Programmiermethoden-CampusMinden/Prog2-Lecture/blob/master/lecture/threads/src/intro/Threaded.java"}
-
Ableiten von
Thread
oder Implementierung vonRunnable
-
Methode
run()
implementieren, aber nicht aufrufen -
Methode
start()
aufrufen, aber (i.d.R.) nicht implementieren
[Demo: creation.*]{.bsp href="https://github.com/Programmiermethoden-CampusMinden/Prog2-Lecture/blob/master/lecture/threads/src/creation/"}
::: notes
start()
startet den Thread und sorgt für Ausführung vonrun()
start()
nur einmal aufrufen
- 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
:::
::: notes
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. :::
\bigskip
- Zugriff auf (
public
) Attribute [(oder eben über Methoden)]{.notes} - Aufruf von Methoden
\bigskip
-
Eine Zeitlang schlafen:
Thread.sleep(<duration_ms>)
::: notes
- 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()
::: notes
- 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()
::: notes
- 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
\bigskip
::: notes 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.
:::
\vspace{24mm}
[Demo: intro.Join]{.bsp href="https://github.com/Programmiermethoden-CampusMinden/Prog2-Lecture/blob/master/lecture/threads/src/intro/Join.java"}
Threads sind weitere Kontrollflussfäden, von Java-VM (oder (selten) von OS) verwaltet
\bigskip
- Ableiten von
Thread
oder implementieren vonRunnable
- Methode
run
enthält den auszuführenden Code - Starten des Threads mit
start
(nie mitrun
!)
::: slides
Unless otherwise noted, this work is licensed under CC BY-SA 4.0. :::