Einleitung: Von grundlegendem synchronized zu modernen Werkzeugen
Java ist eine der wenigen Sprachen, die von Geburt an eine integrierte Unterstützung für Multithreading bietet. Auf der Anfängerebene beschränken sich Entwickler jedoch oft auf das Schlüsselwort synchronized und einfache Thread-Objekte. In realen hochbelasteten Systemen reicht das nicht aus: Sperren verursachen Deadlocks, und das naive Erstellen von Threads führt zu einer Überlastung der JVM.
In diesem Artikel werden wir fortgeschrittene Mechanismen der Parallelität in Java behandeln, die es ermöglichen, effizienten, skalierbaren und sicheren Multithread-Code zu schreiben. Das Material richtet sich an Entwickler, die bereits mit den Grundlagen des Multithreading vertraut sind (Monitore, wait/notify, volatile).
Wir werden vier Schlüsselthemen behandeln: Fork/Join Framework für rekursive Aufgaben, CompletableFuture für asynchrone Programmierung, StampedLock als Alternative zu ReadWriteLock und den sinnvollen Einsatz von ThreadLocal.
1. Fork/Join Framework: Rekursive Aufgabenaufteilung
Das Fork/Join Framework wurde in Java 7 eingeführt und ist für die effiziente Nutzung von Mehrkernprozessoren bei der Lösung von Aufgaben konzipiert, die in Teilaufgaben unterteilt werden können (Teile und herrsche). Die Hauptklasse ist ForkJoinPool, die den Mechanismus des „Arbeitsdiebstahls“ (work-stealing) verwendet.
1.1. Funktionsprinzip
Anstatt Threads manuell zu erstellen, beschreiben Sie eine Aufgabe als Unterklasse von RecursiveTask (wenn ein Rückgabewert vorhanden ist) oder RecursiveAction (ohne Rückgabewert). Dann teilen Sie die Aufgabe rekursiv in Teile auf, bis diese klein genug für die direkte Ausführung sind.
1.2. Beispiel: Parallele Summierung eines Arrays
import java.util.concurrent.RecursiveTask;import java.util.concurrent.ForkJoinPool;
public class SumTask extends RecursiveTask<Long> { private static final int THRESHOLD = 10_000; private final int[] array; private final int start, end;
public SumTask(int[] array, int start, int end) { this.array = array; this.start = start; this.end = end; }
@Override protected Long compute() { if (end - start <= THRESHOLD) { long sum = 0; for (int i = start; i < end; i++) { sum += array[i]; } return sum; } int mid = start + (end - start) / 2; SumTask left = new SumTask(array, start, mid); SumTask right = new SumTask(array, mid, end); left.fork(); // linken Teil asynchron starten long rightResult = right.compute(); // rechten Teil im aktuellen Thread berechnen long leftResult = left.join(); // auf Ergebnis des linken Teils warten return leftResult + rightResult; }
public static void main(String[] args) { int[] data = new int[1_000_000]; for (int i = 0; i < data.length; i++) data[i] = i;
ForkJoinPool pool = ForkJoinPool.commonPool(); long result = pool.invoke(new SumTask(data, 0, data.length)); System.out.println("Summe: " + result); }}Wichtig: Verwenden Sie ForkJoinPool.commonPool() – dieser ist bereits auf die Anzahl der Kerne optimiert. Vermeiden Sie die Erstellung eigener Pools, wenn es nicht notwendig ist.
2. CompletableFuture: Asynchronität ohne Callback-Hölle
Vor Java 8 war asynchrone Programmierung über Future möglich, aber dieses war synchron (blockierender Aufruf von get()). CompletableFuture löst dieses Problem, indem es eine leistungsstarke funktionale API für die Komposition asynchroner Operationen bereitstellt.
2.1. Grundlegende Szenarien
Sie können CompletableFuture erstellen, Aufgaben asynchron starten und sie zu Ketten verknüpfen:
import java.util.concurrent.CompletableFuture;
public cl