Kurz: Mehrere Aufgaben können gleichzeitig ablaufen: Das Programm lädt Daten, während die Oberfläche reagiert, oder verteilt Rechenarbeit auf mehrere Kerne.
Teil des Kurses Java
Mehrere Aufgaben können gleichzeitig ablaufen: Das Programm lädt Daten, während die Oberfläche reagiert, oder verteilt Rechenarbeit auf mehrere Kerne.
Threads
public class Faeden {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try { Thread.sleep(100); } catch (InterruptedException e) { }
System.out.println("t1 fertig");
});
Thread t2 = new Thread(() -> System.out.println("t2 fertig"));
t1.start();
t2.start();
t1.join(); // warten, bis t1 beendet ist
t2.join();
System.out.println("alle fertig");
}
}Ausgabe:
t2 fertig
t1 fertig
alle fertigDas Problem: gemeinsame Daten
Wenn mehrere Threads dieselbe Variable ändern, kommt es zu Wettlaufsituationen (race conditions):
public class Wettlauf {
static int zaehler = 0;
static final Object sperre = new Object();
static void erhoehe(boolean sicher) {
for (int i = 0; i < 100_000; i++) {
if (sicher) synchronized (sperre) { zaehler++; }
else zaehler++; // nicht atomar: Updates gehen verloren
}
}
static int lauf(boolean sicher) throws InterruptedException {
zaehler = 0;
Thread[] t = new Thread[4];
for (int i = 0; i < t.length; i++) { t[i] = new Thread(() -> erhoehe(sicher)); t[i].start(); }
for (Thread x : t) x.join();
return zaehler;
}
public static void main(String[] args) throws InterruptedException {
System.out.println("mit synchronized: " + lauf(true));
System.out.println("ohne Schutz kleiner als 400000: " + (lauf(false) < 400_000));
}
}Ausgabe:
mit synchronized: 400000
ohne Schutz kleiner als 400000: trueLösungen:
| Werkzeug | Zweck |
|---|---|
synchronized | nur ein Thread gleichzeitig im Block |
AtomicInteger, AtomicLong | atomare Zähler ohne Sperre |
ReentrantLock | flexiblere Sperren |
ConcurrentHashMap, CopyOnWriteArrayList | threadsichere Collections |
volatile | Sichtbarkeit einer Variablen für alle Threads |
unveränderliche Objekte (record, final) | brauchen gar keine Sperre |
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
public class Atomar {
public static void main(String[] args) throws Exception {
AtomicInteger z = new AtomicInteger();
ConcurrentHashMap<String, Integer> karte = new ConcurrentHashMap<>();
ExecutorService pool = Executors.newFixedThreadPool(4);
for (int i = 0; i < 4; i++) {
pool.submit(() -> {
for (int k = 0; k < 10_000; k++) { z.incrementAndGet(); karte.merge("x", 1, Integer::sum); }
});
}
pool.shutdown();
pool.awaitTermination(5, TimeUnit.SECONDS);
System.out.println(z.get() + " " + karte.get("x"));
}
}Ausgabe:
40000 40000ExecutorService: Thread-Pools
Threads von Hand zu starten ist aufwendig. Ein Pool verwaltet eine feste Zahl von Threads und führt Aufgaben aus:
import java.util.*;
import java.util.concurrent.*;
public class Pool {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(3);
List<Future<Integer>> ergebnisse = new ArrayList<>();
for (int n = 1; n <= 5; n++) {
final int zahl = n;
ergebnisse.add(pool.submit(() -> { // Callable: liefert ein Ergebnis
Thread.sleep(50);
return zahl * zahl;
}));
}
int summe = 0;
for (Future<Integer> f : ergebnisse) summe += f.get(); // wartet auf das Ergebnis
System.out.println(summe);
pool.shutdown();
}
}Ausgabe:
55CompletableFuture: asynchrone Ketten
import java.util.concurrent.*;
public class Zukunft {
static CompletableFuture<Integer> laden(int id) {
return CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(50); } catch (InterruptedException e) { }
return id * 10;
});
}
public static void main(String[] args) throws Exception {
CompletableFuture<Integer> a = laden(1), b = laden(2);
CompletableFuture<Integer> summe = a.thenCombine(b, Integer::sum);
System.out.println(summe.thenApply(x -> "Summe: " + x).get());
CompletableFuture<String> fehler = CompletableFuture
.<String>supplyAsync(() -> { throw new IllegalStateException("kaputt"); })
.exceptionally(e -> "Ersatzwert (" + e.getCause().getMessage() + ")");
System.out.println(fehler.get());
CompletableFuture.allOf(laden(1), laden(2), laden(3)).get();
System.out.println("alle drei fertig");
}
}Ausgabe:
Summe: 30
Ersatzwert (kaputt)
alle drei fertigVirtual Threads (Java 21)
Virtual Threads sind leichtgewichtige Threads, von denen man Hunderttausende starten kann. Sie machen das einfache Programmiermodell “ein Thread pro Anfrage” auch für sehr viele gleichzeitige Verbindungen möglich:
import java.util.concurrent.*;
import java.util.stream.*;
public class Virtuell {
public static void main(String[] args) throws Exception {
try (var pool = Executors.newVirtualThreadPerTaskExecutor()) {
var aufgaben = IntStream.range(0, 1000).mapToObj(i -> pool.submit(() -> {
Thread.sleep(10); // blockiert nur den virtuellen Thread
return i;
})).toList();
long summe = 0;
for (var f : aufgaben) summe += f.get();
System.out.println(summe);
}
}
}Ausgabe:
499500Regeln für nebenläufigen Code
- Unveränderlich wo möglich:
final,record,List.of - Gemeinsamen Zustand vermeiden oder klar schützen
- Höhere Werkzeuge (
ExecutorService,CompletableFuture,ConcurrentHashMap) statt roher Threads - Threads nicht per
Thread.stop()beenden: mitinterrupt()und PrüfungisInterrupted() - Deadlocks vermeiden: Sperren immer in derselben Reihenfolge nehmen
- Testen ist schwer: Nebenläufigkeit mit Werkzeugen und Belastungstests prüfen
Merke
Thread,Runnable,start(),join()sind die Grundlage; besser sind Pools (ExecutorService)- Gemeinsame Daten schützen:
synchronized,Atomic*,ConcurrentHashMap FutureundCompletableFutureliefern Ergebnisse asynchron- Virtual Threads (Java 21) erlauben sehr viele gleichzeitige Aufgaben
- Unveränderliche Objekte vermeiden Probleme von vornherein
Übungsaufgabe
Berechne die Summe von 1 bis 1.000.000 aufgeteilt auf 4 Aufgaben in einem Thread-Pool und füge die Teilergebnisse zusammen.
Quiz zur Selbstkontrolle
Was ist eine Wettlaufsituation?
- Mehrere Threads ändern gemeinsame Daten ohne Schutz, das Ergebnis ist unvorhersehbar (richtig)
- Zwei Programme laufen gegeneinander
- Ein schneller Thread
- Ein Fehler im Compiler
Wofür ist AtomicInteger?
- Ein threadsicherer Zähler ohne explizite Sperre (richtig)
- Eine Atom-Simulation
- Ein großer Integer
- Ein Integer nur für Konstanten
Wozu dient ein ExecutorService?
- Er verwaltet einen Pool von Threads und führt Aufgaben aus (richtig)
- Er startet das Betriebssystem
- Er kompiliert Code
- Er sendet E-Mails
Weiter im Kurs
Zurück: Datum und Zeit
Weiter: Tests, Pakete und Projektaufbau
Alle Kapitel: Java im Überblick