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 fertig
Das 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: true
Lö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 40000
ExecutorService: 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
55
CompletableFuture: 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 fertig
Virtual 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
499500
Regeln 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
Aufgabe
Berechne die Summe von 1 bis 1.000.000 aufgeteilt auf 4 Aufgaben in einem Thread-Pool und füge die Teilergebnisse zusammen.