Threads
Kurz gesagt: Ein Thread ist ein eigener Ausführungsstrang im Programm; mehrere Threads laufen gleichzeitig und teilen sich den Speicher, deshalb wird ein gemeinsamer Zähler mit synchronized geschützt.
Problem
Drei Berichte, einer nach dem anderen
Thread.sleep(ms) hält das Programm für so viele Millisekunden an. Die Pause kann von außen unterbrochen werden und wirft dann die geprüfte InterruptedException, deshalb fängt download sie.static void download(String file, int ms) { try { Thread.sleep(ms); } catch (InterruptedException e) { return; } System.out.println("Heruntergeladen: " + file);}
public static void main(String[] args) { download("maerz.pdf", 600); download("april.pdf", 200); download("mai.pdf", 400); System.out.println("Fertig");}Heruntergeladen: maerz.pdfHeruntergeladen: april.pdfHeruntergeladen: mai.pdfFertigDie Idee
Prozess und Thread
Wenn du das Programm startest, gibt ihm das Betriebssystem einen Prozess: eigenen Speicher, eigene geöffnete Dateien. Im Prozess laufen ein oder mehrere Threads. Ein Thread ist eine Folge von Befehlen, die nacheinander ausgeführt werden. Bisher hatte jedes deiner Programme einen Thread: den, der main ausführt.
Die Threads eines Prozesses teilen sich ihren Speicher: Sie sehen dieselben Objekte. Das macht sie praktisch (sie können einander leicht Daten geben) und gefährlich (sie können einander durcheinanderbringen, wie du gleich sehen wirst).
- Parallel heißt wirklich gleichzeitig, auf verschiedenen Kernen des Prozessors.
- Nebenläufig (concurrent) heißt auf einem Kern, abwechselnd und so schnell, dass es gleichzeitig aussieht.
Für dich als Programmierer spielt der Unterschied selten eine Rolle: In beiden Fällen weißt du nicht, welcher Thread wann weitermacht.
Genauer, für Neugierige
- Der Browser Chrome startet für jeden Tab einen eigenen Prozess. Deshalb hält eine hängende Seite die anderen nicht auf, und deshalb braucht der Browser auch viel Speicher.
- Threads benutzt jedes Programm, das mehrere Dinge auf einmal tut: Ein Spiel spielt den Ton in einem Thread ab und führt die Logik in einem anderen aus; ein Webserver bedient jeden Client in seinem eigenen Thread.
- Der Preis: Jeder Thread hat seinen eigenen Stack, und der Speicher dafür wird extra reserviert. Tausende Threads kosten viel. Deshalb nehmen echte Programme einen Thread-Pool (die letzte Idee der Lektion).
Sieh es dir an
Der erste Thread
Die Klasse Thread ist ein Thread. Ihrem Konstruktor gibst du mit, was er tun soll: ein Objekt des Interfaces Runnable, das eine Methode hat, run(). Am kürzesten geht es mit einem Lambda:
Thread worker = new Thread(() -> download("april.pdf", 200));worker.start();System.out.println("main wartet nicht");worker.join();System.out.println("Der Bericht ist heruntergeladen");main wartet nichtHeruntergeladen: april.pdfDer Bericht ist heruntergeladenstart()startet den neuen Thread und kehrt sofort zurück.mainmacht weiter, ohne zu warten, deshalb steht „main wartet nicht“ zuerst da.join()lässtmainwarten, bis der Thread fertig ist.
download ist die Methode aus der ersten Karte. Gut, dass sie die InterruptedException selbst fängt: run() in Runnable hat kein throws, also kann der Code im Thread die geprüfte Exception nicht nach außen lassen. Auch join() wirft sie, deshalb ist main ab hier public static void main(String[] args) throws InterruptedException.
Genauer, für Neugierige
class ReportThread extends Thread { @Override public void run() { System.out.println("1. eine Unterklasse von Thread"); }}
class ReportJob implements Runnable { @Override public void run() { System.out.println("2. eine Klasse, die Runnable implementiert"); }}
public static void main(String[] args) throws InterruptedException { Thread a = new ReportThread(); Thread b = new Thread(new ReportJob()); Thread c = new Thread(new Runnable() { @Override public void run() { System.out.println("3. eine anonyme Klasse"); } }); Thread d = new Thread(() -> System.out.println("4. ein Lambda")); for (Thread t : new Thread[] { a, b, c, d }) { t.start(); t.join(); }}1. eine Unterklasse von Thread2. eine Klasse, die Runnable implementiert3. eine anonyme Klasse4. ein LambdaThread ist der älteste Weg. Flexibler ist es, die Aufgabe (Runnable) vom Thread zu trennen: So kann dieselbe Aufgabe an einen Thread-Pool gehen, und deine Klasse kann weiter von einer anderen Klasse erben.Sieh es dir an
Drei auf einmal
long start = System.currentTimeMillis();Thread march = new Thread(() -> download("maerz.pdf", 600));Thread april = new Thread(() -> download("april.pdf", 200));Thread may = new Thread(() -> download("mai.pdf", 400));march.start();april.start();may.start();march.join();april.join();may.join();long ms = System.currentTimeMillis() - start;System.out.println("Fertig in unter einer Sekunde: " + (ms < 1000));Heruntergeladen: april.pdfHeruntergeladen: mai.pdfHeruntergeladen: maerz.pdfFertig in unter einer Sekunde: trueDie Threads starten in der Reihenfolge März, April, Mai, werden aber in der Reihenfolge der Pausen fertig: April (0,2 s), Mai (0,4 s), März (0,6 s). Das ganze Warten dauert so lange wie das längste, 0,6 Sekunden statt 1,2.
In welcher Reihenfolge die Threads beiprintln ankommen, entscheidet das Betriebssystem. Hier sind die Pausen unterschiedlich genug, also ist die Reihenfolge immer gleich. Wären sie gleich lang, könnte sie bei jedem Lauf anders sein.Probier es aus
start oder run
Was gibt das Programm aus? (das zweite Argument von Thread ist der Name des Threads)
Thread t = new Thread(() -> System.out.println("Läuft in: " + Thread.currentThread().getName()), "downloader");t.run();t.start();t.join();t.run() ist ein gewöhnlicher Methodenaufruf: Das Lambda läuft sofort, im Thread von main. Einen neuen Thread gibt es nicht. Erst t.start() startet den Thread „downloader“, der dasselbe Lambda ausführt. run() statt start() zu schreiben ist ein häufiger Fehler: Das Programm funktioniert, nur läuft nichts parallel.Die Idee
Das Leben eines Threads
getState() sagt dir, in welchem er ist:Thread t = new Thread(() -> download("mai.pdf", 500));System.out.println(t.getState());t.start();Thread.sleep(100);System.out.println(t.getState());t.join();System.out.println(t.getState());NEWTIMED_WAITINGHeruntergeladen: mai.pdfTERMINATED| Zustand | Wann |
|---|---|
NEW |
erzeugt, noch nicht mit start() gestartet |
RUNNABLE |
läuft oder ist bereit zu laufen |
BLOCKED, WAITING, TIMED_WAITING |
wartet: auf einen Lock, auf einen anderen Thread (join) oder eine bestimmte Zeit (sleep) |
TERMINATED |
run() ist fertig |
Ein Thread kann nur einmal gestartet werden. Ein zweites start() auf demselben Thread wirft IllegalThreadStateException.
Genauer, für Neugierige
Weitere Methoden von Thread:
interrupt()bittet den Thread, aufzuhören. Schläft oder wartet er, wirftsleep/joineineInterruptedException. Deshalb fangen wir sie indownload.getName(),threadId()(frühergetId()): Name und Nummer des Threads.Thread.currentThread()gibt dir den Thread, in dem der Code gerade läuft.Thread.yield()deutet an, dass der Thread den Prozessor einem anderen überlassen kann. Das brauchst du selten.
Problem
Die verlorenen Besuche
increment() auf:class Counter { private int count = 0;
public void increment() { count++; }
public int getCount() { return count; }}
public static void main(String[] args) throws InterruptedException { Counter visits = new Counter(); Runnable work = () -> { for (int i = 0; i < 100_000; i++) { visits.increment(); } }; Thread first = new Thread(work); Thread second = new Thread(work); first.start(); second.start(); first.join(); second.join(); System.out.println("Besuche: " + visits.getCount());}Du erwartest 200 000. Fünf Läufe:
Besuche: 150329Besuche: 138149Besuche: 168862Besuche: 165672Besuche: 157704Jedes Mal weniger, und jedes Mal anders. Der Grund ist, dass count++ kein einzelner Schritt ist, sondern drei:
int value = count; // 1. lesenvalue = value + 1; // 2. 1 addierencount = value; // 3. schreibenclass Counter { private int count = 5;
public void increment() { count++; // lesen, 1 addieren, schreiben }
public synchronized void safeIncrement() { count++; }
public synchronized int getCount() { return count; }}Schritt für Schritt, in Worten
- count ist 5. Die Threads A und B rufen gleichzeitig increment() auf.
- A liest count: 5.
- B liest count, bevor A geschrieben hat: auch 5.
- A addiert 1 in seiner eigenen Kopie und schreibt 6.
- B schreibt auch 6. Zwei Aufrufe, und count ist 6, nicht 7: Eine der Additionen geht verloren.
- Noch einmal, jetzt mit synchronized: A nimmt den Lock, liest 5 und schreibt 6.
- B erreicht die Methode und wartet, bis A den Lock freigibt.
- B nimmt den Lock, liest 6 und schreibt 7: Nichts geht verloren.
Wenn beide Threads denselben Wert lesen, bevor einer von ihnen geschrieben hat, geht eine der Additionen verloren. Das nennt man eine Race Condition (Wettlaufsituation). Ob sie eintritt, hängt davon ab, wann das Betriebssystem die Threads wechselt, deshalb ist das Ergebnis bei jedem Lauf anders.
Die Idee
synchronized: einer nach dem anderen
synchronized vor der Methode tut genau das:class Counter { private int count = 0;
public synchronized void increment() { count++; }
public synchronized int getCount() { return count; }}Besuche: 200000Jedes Objekt hat einen Lock. Der Thread nimmt ihn, bevor er eine synchronized-Methode betritt, und gibt ihn frei, wenn er sie verlässt. Der andere Thread, der dasselbe Objekt will, wartet. Jetzt ist das Ergebnis bei jedem Lauf 200 000. Auch getCount() ist synchronized: So sieht der Thread, der liest, den zuletzt geschriebenen Wert.
Musst du nur einen Teil einer Methode sperren, gibt es einen synchronized-Block mit einem Objekt für den Lock. Für einen Zähler gibt es auch eine fertige Klasse: AtomicInteger (aus java.util.concurrent.atomic), deren incrementAndGet() die drei Schritte auf einmal macht, ohne Lock:
class Counter { private int count = 0; private final Object lock = new Object();
public void increment() { synchronized (lock) { count++; } }
public int getCount() { synchronized (lock) { return count; } }}class Counter { private final AtomicInteger count = new AtomicInteger();
public void increment() { count.incrementAndGet(); }
public int getCount() { return count.get(); }}Besuche: 200000Genauer, für Neugierige
wait()undnotifyAll(): Ein Thread in einer synchronized-Methode kann den Lock freigeben und schlafen, bis ein anderer Thread ihn weckt. So wartet die Küche auf eine Bestellung, und die Kasse wartet, bis die Küche die vorige genommen hat (das Problem „Producer und Consumer“):
class OrderBox { private String order;
public synchronized void put(String newOrder) throws InterruptedException { while (order != null) { wait(); } order = newOrder; notifyAll(); }
public synchronized String take() throws InterruptedException { while (order == null) { wait(); } String result = order; order = null; notifyAll(); return result; }}
public static void main(String[] args) throws InterruptedException { OrderBox box = new OrderBox(); Thread kitchen = new Thread(() -> { try { for (int i = 0; i < 3; i++) { System.out.println("Die Küche nimmt " + box.take()); } } catch (InterruptedException e) { return; } }); kitchen.start(); box.put("Pizza"); box.put("Salat"); box.put("Suppe"); kitchen.join();}Die Küche nimmt PizzaDie Küche nimmt SalatDie Küche nimmt Suppewait() steht in einem while, nicht in einem if: Der Thread kann auch ohne Grund aufwachen, deshalb prüft er die Bedingung noch einmal.
- Deadlock (Verklemmung): Zwei Threads warten für immer aufeinander. Das Büro hält den Drucker und wartet auf den Scanner, und der Empfang hält den Scanner und wartet auf den Drucker (
pauseistsleepmit gefangener Exception):
Object printer = new Object();Object scanner = new Object();
Thread office = new Thread(() -> { synchronized (printer) { pause(100); synchronized (scanner) { System.out.println("Das Büro kopiert"); } }});Thread reception = new Thread(() -> { synchronized (scanner) { pause(100); synchronized (printer) { System.out.println("Der Empfang kopiert"); } }});Das Programm hängt und gibt nie etwas aus. Vermeiden lässt sich das, indem alle Threads die Locks in derselben Reihenfolge nehmen.
- Prioritäten:
setPriorityvon 1 bis 10 deutet an, wie wichtig der Thread ist. Das Betriebssystem kann es ignorieren, also verlass dich nicht auf Prioritäten, damit das Programm richtig ist:
Thread report = new Thread(() -> download("jahresbericht.pdf", 100));report.setPriority(Thread.MIN_PRIORITY); // 1; NORM_PRIORITY ist 5, MAX_PRIORITY ist 10System.out.println(report.getName() + ", priority " + report.getPriority());Probier es aus
Zwei Geldautomaten
Wähle die Zeile mit dem Fehler.
withdraw ist nicht synchronized. Die zwei Threads prüfen balance >= amount gleichzeitig, beide sehen 100, und beide heben ab. Die Prüfung und die Abhebung müssen ein Ganzes sein: public synchronized void withdraw(int amount) (und synchronized für getBalance). Dann wartet der zweite Geldautomat auf den ersten, sieht 20 € und lehnt ab. Der Kontostand bleibt 20 €.Die Idee
Thread-Pool und Future
ExecutorService (aus java.util.concurrent):static int sizeOf(String file, int ms) { try { Thread.sleep(ms); } catch (InterruptedException e) { return -1; } return file.length() * 100;}String[] files = { "maerz.pdf", "april.pdf", "mai.pdf", "juni.pdf" };ExecutorService pool = Executors.newFixedThreadPool(2);List<Future<Integer>> sizes = new ArrayList<>();for (String file : files) { sizes.add(pool.submit(() -> sizeOf(file, 300)));}for (int i = 0; i < files.length; i++) { System.out.println(files[i] + ": " + sizes.get(i).get() + " KB");}pool.shutdown();maerz.pdf: 900 KBapril.pdf: 900 KBmai.pdf: 700 KBjuni.pdf: 800 KBExecutors.newFixedThreadPool(2)erzeugt einen Pool aus 2 Threads. Die vier Aufgaben wechseln sich darin ab.submitnimmt eine Aufgabe, die ein Ergebnis zurückgibt, und gibt dir sofort einFuture: das Versprechen, dass das Ergebnis kommt.get()wartet, bis die Aufgabe fertig ist, und gibt das Ergebnis zurück.shutdown()sagt dem Pool, keine neuen Aufgaben mehr anzunehmen und die Threads zu beenden, wenn sie fertig sind. Ohne das endet das Programm nicht: Die Threads des Pools warten auf weitere Arbeit.
main hat hier throws Exception, weil get() auch eine ExecutionException wirft, wenn die Aufgabe eine Exception geworfen hat.
Genauer, für Neugierige
execute. Dann wartest du nach shutdown mit awaitTermination auf alle:ExecutorService pool = Executors.newFixedThreadPool(2);for (int i = 1; i <= 5; i++) { String file = "bericht-" + i + ".pdf"; pool.execute(() -> download(file, 100));}pool.shutdown();pool.awaitTermination(1, TimeUnit.MINUTES);System.out.println("Alles fertig.");Heruntergeladen: bericht-2.pdfHeruntergeladen: bericht-1.pdfHeruntergeladen: bericht-3.pdfHeruntergeladen: bericht-4.pdfHeruntergeladen: bericht-5.pdfAlles fertig.while (!executor.isTerminated()) { }. Das wartet auch, lässt den Prozessor aber umsonst rotieren. awaitTermination schläft, bis der Pool fertig ist.Probier es aus
Welches Ergebnis kommt zuerst?
„archiv.zip“ braucht 0,6 s zum Herunterladen, „notiz.txt“ 0,1 s. Was gibt das Programm aus?
ExecutorService pool = Executors.newFixedThreadPool(2);Future<Integer> big = pool.submit(() -> sizeOf("archiv.zip", 600));Future<Integer> small = pool.submit(() -> sizeOf("notiz.txt", 100));System.out.println("Groß: " + big.get());System.out.println("Klein: " + small.get());pool.shutdown();Die kleine Datei ist wirklich zuerst fertig. Aber big.get() steht in der ersten Zeile und wartet auf die große. Wenn die da ist, gibt small.get() sofort zurück, weil das Ergebnis schon da ist. Die Reihenfolge der Ausgabe ist die Reihenfolge der get()-Aufrufe, deshalb ist sie bei jedem Lauf gleich.
Übung
Programme mit Threads
Löse die Aufgaben in deinem Editor. main darf throws InterruptedException haben (oder throws Exception für Future.get()).
1. Parallele Summe. Das Array sales enthält 1 000 000 Verkäufe, sales[i] = i % 100. Teile es in 4 gleiche Teile und summiere jeden in seinem eigenen Thread. Jeder Thread schreibt an seinen eigenen Platz im Array long[] partial, deshalb brauchst du keine Synchronisierung. Warte mit join auf sie und addiere die Teile:
Teile: [12375000, 12375000, 12375000, 12375000]Gesamt: 495000002. Herunterladen mit einem Pool. Lade sechs Berichte (maerz.pdf, april.pdf, mai.pdf, juni.pdf, juli.pdf, august.pdf) mit einem Pool aus 3 Threads herunter. Benutze sizeOf(file, 300) aus der Karte zum Pool. Gib die Größen in der Reihenfolge der Dateien aus, die Summe und ob alles in unter 1,5 Sekunden fertig war (nacheinander wären es 1,8 s):
maerz.pdf: 900 KBapril.pdf: 900 KBmai.pdf: 700 KBjuni.pdf: 800 KBjuli.pdf: 800 KBaugust.pdf: 1000 KBGesamt: 5100 KBUnter 1,5 Sekunden: true3. Ein Zähler ohne Verluste. Vier Threads mit je 50 000 Besuchen erhöhen einen gemeinsamen Zähler. Schreib den Zähler so, dass das Ergebnis immer 200 000 ist. Probier es zuerst ohne Synchronisierung und sieh, wie viel verloren geht, dann korrigiere es einmal mit synchronized und einmal mit AtomicInteger.
Besuche: 200000Weitere Aufgaben
- Ein Test für den Zähler. Schreib einen JUnit-Test, der 8 Threads mit je 10 000 Erhöhungen startet und mit
assertEqualsauf 80 000 prüft. Lass ihn ein paarmal gegen den nicht synchronisierten Zähler laufen. Ein Test, der nur manchmal fehlschlägt, ist das typische Zeichen einer Race Condition. - Der Deadlock. Führe das Beispiel mit dem Drucker und dem Scanner aus „Genauer“ aus und sieh zu, wie es hängt. Korrigiere es so, dass beide Threads zuerst den Drucker nehmen.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Listen · Klassen in Klassen · Lambdas · Tests mit JUnit
- Geht weiter in: Netzwerkprogrammierung · Quicksort und der schlechte Pivot · Der Aufrufstapel von innen · Von der Rekursion zur Schleife
- Aufgaben zum Thema: Programme in der echten Welt