Java Concurrency Masterclass: JMM, Lock-Free Atomics & Virtual Threads (Loom)
Erstellt: 07.11.2026 Zuletzt aktualisiert: 07.11.2026
Executive Summary: Moderne Mehrkernprozessoren erzielen enormen Durchsatz durch aggressive Out-of-Order-Befehlsausführung, spekulative Pipelines und mehrstufige Cache-Hierarchien (L1, L2, L3). Parallel laufende Threads können sich nicht auf intuitive Lese-/Schreibreihenfolgen verlassen; ihr Verhalten wird formal durch das Java Memory Model (JMM / JSR-133) und Hardware-Speicherbarrieren bestimmt. Bei stark umkämpften Ressourcen eliminieren sperrenfreie Compare-And-Swap (CAS) Primitiven teure Kernel-Futex-Kontextwechsel, während Project Loom Virtual Threads die Skalierung der JVM vollständig von nativen Linux-Thread-Limits entkoppeln. Dieser Leitfaden bildet Teil 3.1 (Advanced & Deep Dive) unseres Java-Curriculums: Er dekonstruiert Speicherbarrieren und Happens-Before-Garantien, analysiert lock-freie Atomics und
StampedLock, erklärt Continuations im Loom-Scheduler und führt einen Benchmark mit 50.000 parallelen blockierenden Tasks durch.
-
Java Memory Model (JMM)
Happens-Before-Garantien, Cache-Kohärenz (MESI), Store-Buffer, Hardware-Speicherbarrieren (
mfence) und False Sharing analysieren. -
Lock-Free Atomics & CAS
Hardware-Instruktionen (
cmpxchg) nutzen, das ABA-Problem mit Versions-Stamps lösen und Contention mitLongAddereliminieren. -
Explizite Locks & StampedLock
AQS-Warteschlangen, Monitor-Inflation (vom Thin Lock zum Fat OS Futex) und sperrenfreie optimistische Leseoperationen via
StampedLockverstehen. -
50k Virtual Threads Praxislabor
50.000 parallele blockierende I/O-Tasks benchmarken und die Sättigung klassischer Plattform-Threadpools mit Project Loom vergleichen.
1. Das Java Memory Model (JMM) & CPU-Hardware-Realitäten
Auf moderner Multiprozessor-Hardware (SMP) schreiben CPU-Kerne Daten nicht sofort in das zentrale RAM, sondern in lokale Store Buffers und Zwischen-Caches (L1/L2), die über Cache-Kohärenzprotokolle (wie MESI) synchronisiert werden:
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
subgraph MultiCoreCPU ["Multi-Core CPU Hardware-Topologie"]
subgraph Core1 ["CPU Core 0"]
REG1["Register"]
SB1["Store Buffer (Out-of-Order)"]
L1_1["L1 Daten-Cache (32 KB)"]
REG1 --> SB1 --> L1_1
end
subgraph Core2 ["CPU Core 1"]
REG2["Register"]
SB2["Store Buffer (Out-of-Order)"]
L1_2["L1 Daten-Cache (32 KB)"]
REG2 --> SB2 --> L1_2
end
L3["Gemeinsamer L3 Cache (32 MB)"]
L1_1 --> L3
L1_2 --> L3
end
RAM["Hauptspeicher (DDR5 RAM)"]
L3 ==> RAM
Weil CPU-Kerne ihre Store-Buffer asynchron leeren, um die Rechenwerke nicht zu blockieren, entstehen ohne explizite Barrieren Befehlsumordnungen (Instruction Reordering) und Sichtbarkeitsfehler:
Die Happens-Before-Garantie (JSR-133)
Das JMM definiert eine partielle Ordnung über alle Speicherzugriffe. Wenn Operation $A$ happens-before Operation $B$ gilt ($A \prec B$), ist garantiert, dass $B$ alle Zustandsänderungen von $A$ sieht:
- Programmreihenfolge-Regel: Jede Aktion in einem Thread geschieht vor jeder späteren Aktion in demselben Thread.
- Monitor-Lock-Regel: Das Freigeben (
unlock) eines Monitors geschieht vor jedem nachfolgenden Sperren (lock) desselben Monitors. - Volatile-Variablen-Regel: Ein Schreibzugriff auf ein
volatile-Feld geschieht vor jedem nachfolgenden Lesezugriff auf dasselbe Feld. - Thread-Start/Join-Regel: Der Aufruf von
thread.start()geschieht vor allen Aktionen im gestarteten Thread; alle Aktionen eines Threads geschehen vor der Rückkehr austhread.join().
Assembly-Mechanik von volatile
Auf x86_64-Architekturen ist das Lesen von RAM von Natur aus geordnet. Ein Schreibzugriff auf eine volatile-Variable veranlasst HotSpot jedoch, eine Hardware-Speicherbarriere zu emittieren:
Dies zwingt die CPU, alle vorangegangenen Schreibzugriffe in den L1/L2-Cache zu flushen und verbietet JIT-Compiler-Umordnungen.
2. Lock-Free Programmierung: Compare-And-Swap (CAS) & Atomics
Herkömmliche Sperren schläfern Threads über Betriebssystem-Kernel-Kontextwechsel (futex-Syscalls unter Linux) ein, was pro Konflikt ca. 1.000–5.000 CPU-Taktzyklen kostet. Sperrenfreie (Lock-Free) Algorithmen nutzen stattdessen atomare Hardware-Instruktionen.
Das Compare-And-Swap (CAS) Primitiv
Die CPU führt Vergleich und Zuweisung in einem einzigen atomaren Hardware-Zyklus aus:
$$\text{CAS}(V, E, N): \quad \text{wenn } V == E \implies V = N, \text{ liefere true; sonst liefere false;}$$
public final int incrementAndGet() {
return U.getAndAddInt(this, VALUE, 1) + 1; // Bildet direkt auf die CPU-Instruktion 'lock cmpxchg' ab!
}
Das ABA-Problem & Versions-Stamps
Liest Thread 1 den Wert $A$, ändert Thread 2 den Wert auf $A \to B \to A$, dann gelingt das CAS von Thread 1 fälschlicherweise, obwohl der Zwischenzustand manipuliert wurde.
- Lösung:
AtomicStampedReference<V>koppelt die Objektreferenz mit einer ganzzahligen Versionsnummer, die bei jeder Änderung streng monoton ansteigt.
Skalierung unter Hochlast: LongAdder vs. AtomicLong
Unter extremer Last (z.B. 64 CPU-Kerne inkrementieren dieselbe Variable) bricht AtomicLong ein, weil alle Kerne um dieselbe Cache-Line kämpfen (Cache-Line-Bouncing).
LongAdder löst dies durch Cell-Striping:
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
subgraph LongAdderTopology ["LongAdder Cell-Striping Architektur"]
BASE["Basis-Zähler (Geringe Last)"]
C1["Cell [0] (Core 0 Zähler)"]
C2["Cell [1] (Core 1 Zähler)"]
C3["Cell [2] (Core 2 Zähler)"]
C4["Cell [3] (Core 3 Zähler)"]
end
SUM["Gesamtsumme = Basis + Cell[0] + Cell[1] + Cell[2] + Cell[3]"]
BASE --> SUM
C1 --> SUM
C2 --> SUM
C3 --> SUM
C4 --> SUM
Threads hashen auf unabhängige, gepaddete Cell-Objekte. Konflikte werden eliminiert; aufsummiert wird erst bei Aufruf von .sum().
3. Explizite Locks & Optimistisches Lesen (StampedLock)
Während ReentrantLock über den AbstractQueuedSynchronizer (AQS) Timeout-fähige, faire Sperren ermöglicht, setzt man bei extrem vielen Lesezugriffen auf StampedLock.
Das optimistische Lese-Idiom
Im Gegensatz zu ReentrantReadWriteLock, wo ein geteiltes Lese-Lock schreibende Threads blockiert, erlaubt StampedLock optimistisches Lesen völlig ohne echtes Sperren:
public class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
public double distanceFromOrigin() {
// 1. Optimistischen Lese-Stempel abrufen (KEIN Lock gesetzt, Schreiber laufen unblockiert!)
long stamp = sl.tryOptimisticRead();
double currentX = x, currentY = y;
// 2. Prüfen, ob während des Lesens ein Schreiber aktiv war
if (!sl.validate(stamp)) {
// 3. Fallback auf ein pessimistisches echtes Lese-Lock
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
4. Project Loom: Virtual Threads Architektur (Java 21 LTS)
Historisch bildete Java jeden java.lang.Thread 1:1 auf einen nativen Linux-Kernel-Thread (pthread) ab. Da jeder Thread standardmäßig 1 MB Stack-Speicher (-Xss) reserviert, ist ein Server nach 5.000–10.000 Threads speicherseitig erschöpft.
Das M:N User-Mode Scheduling
Java 21 führt Virtual Threads ein: extrem leichtgewichtige Userspace-Threads, die von der HotSpot-JVM auf einen kleinen Pool von Carrier-OS-Threads gemountet werden (ForkJoinPool):
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
subgraph VirtualThreadsLayer ["1.000.000+ Virtual Threads (Heap-Continuations)"]
VT1["Virtual Thread #1 (Socket I/O)"]
VT2["Virtual Thread #2 (Datenbankabfrage)"]
VT3["Virtual Thread #3 (HTTP-Request)"]
VT4["Virtual Thread #4 (Blockiert auf epoll)"]
end
subgraph CarrierPool ["Carrier Plattform-Threads (ForkJoinPool = CPU Cores)"]
C1["Carrier Thread 0 (OS Thread)"]
C2["Carrier Thread 1 (OS Thread)"]
end
subgraph LinuxKernel ["Debian Linux Kernel"]
EPOLL["Nicht-blockierender epoll Deskriptor-Monitor"]
end
VT1 ==>|"Gemountet"| C1
VT2 ==>|"Gemountet"| C2
VT3 -.->|"Stack auf Heap unmountet"| EPOLL
VT4 -.->|"Stack auf Heap unmountet"| EPOLL
- Mounting & Ausführung: Ein virtueller Thread mountet auf einen Carrier-Thread und führt normalen synchronen Code aus.
- Unmounting bei blockierendem I/O: Sobald der virtuelle Thread eine blockierende Operation ausführt (
Socket,JDBC,Thread.sleep), friert die JVM seinen Stack-Frame auf dem JVM-Heap ein, unmountet den Thread und gibt den Träger-OS-Thread sofort frei! - Epoll-Benachrichtigung: Sobald Linux
epollmeldet, dass Daten vorliegen, plant die JVM die Continuation auf einem beliebigen freien Carrier-Thread wieder ein.
[!WARNING] Die Thread-Pinning-Gefahr Betritt ein virtueller Thread einen
synchronized-Block oder eine native JNI-Methode und blockiert darin auf I/O, wird er an seinen Carrier-Thread gepinnt und blockiert diesen für alle anderen Virtual Threads! Industrieregel: Ersetzesynchronizedin Hochlast-Pfaden durchReentrantLock, das voll Loom-kompatibel ist und niemals Threads pinnt.
5. Hands-On Systems Lab: VirtualThreadBenchmarkLab.java
Wir implementieren nun einen Benchmark mit 50.000 parallelen blockierenden I/O-Tasks, um die Überlegenheit von Project Loom live zu messen:
- Simuliert realistische 50 ms Netzwerk-/Datenbank-Latenz pro Task.
- Führt 50.000 Tasks auf einem klassischen Plattform-Thread-Pool (fest 200 OS-Threads) aus.
- Führt 50.000 Tasks mit Virtual Threads (Executors.newVirtualThreadPerTaskExecutor()) aus.
- Misst Ausführungsdauer, Durchsatz und Systemverhalten.
- Vollständig lauffähig via JEP 330 (java VirtualThreadBenchmarkLab.java).
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class VirtualThreadBenchmarkLab {
private static final int TOTAL_TASKS = 50_000;
private static final int SIMULATED_IO_LATENCY_MS = 50;
public static void main(String[] args) throws InterruptedException {
System.out.println("=== PROJECT LOOM VIRTUAL THREADS BENCHMARK LAB ===");
System.out.println("Verfügbare CPU-Kerne: " + Runtime.getRuntime().availableProcessors());
System.out.println("Parallele blockierende Tasks: " + TOTAL_TASKS + " (je 50ms I/O Latenz)\n");
// 1. Ausführung mit herkömmlichem Plattform-Threadpool (fest 200 OS Threads)
System.out.println("--- Test A: Plattform-Thread-Pool (Fest 200 OS-Threads) ---");
try (ExecutorService platformExecutor = Executors.newFixedThreadPool(200)) {
runBenchmark(platformExecutor, "Plattform-Threads (Pool=200)");
}
// Kurze Pause zur Stabilisierung
Thread.sleep(1000);
// 2. Ausführung mit Project Loom Virtual Threads
System.out.println("\n--- Test B: Project Loom Virtual Threads (1 Task = 1 Virtual Thread) ---");
try (ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
runBenchmark(virtualExecutor, "Virtual Threads (Unbeschränkt)");
}
System.out.println("\nVirtual Threads Systems-Benchmark erfolgreich abgeschlossen.");
}
private static void runBenchmark(ExecutorService executor, String testName) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(TOTAL_TASKS);
AtomicInteger successCounter = new AtomicInteger(0);
Instant start = Instant.now();
for (int i = 0; i < TOTAL_TASKS; i++) {
executor.submit(() -> {
try {
// Simuliert blockierendes Socket-Read oder Datenbankabfrage
Thread.sleep(Duration.ofMillis(SIMULATED_IO_LATENCY_MS));
successCounter.incrementAndGet();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown();
}
});
}
// Warten, bis alle 50.000 Tasks beendet sind
latch.await();
Instant finish = Instant.now();
long durationMs = Duration.between(start, finish).toMillis();
double throughputPerSec = ((double) TOTAL_TASKS / durationMs) * 1000.0;
System.out.printf("Ergebnisse für [%s]:\n", testName);
System.out.printf(" Erfolgreiche Tasks: %d / %d\n", successCounter.get(), TOTAL_TASKS);
System.out.printf(" Benötigte Zeit: %d ms (%.2f Sekunden)\n", durationMs, durationMs / 1000.0);
System.out.printf(" Durchsatz: %.2f Tasks/Sekunde\n", throughputPerSec);
}
}
Kompilieren und Ausführen unter Linux
Führe das Benchmark-Programm direkt ohne manuelles Kompilieren aus:
Erwartete Terminal-Ausgabe:
=== PROJECT LOOM VIRTUAL THREADS BENCHMARK LAB ===
Verfügbare CPU-Kerne: 8
Parallele blockierende Tasks: 50000 (je 50ms I/O Latenz)
--- Test A: Plattform-Thread-Pool (Fest 200 OS-Threads) ---
Ergebnisse für [Plattform-Threads (Pool=200)]:
Erfolgreiche Tasks: 50000 / 50000
Benötigte Zeit: 12840 ms (12.84 Sekunden)
Durchsatz: 3894.08 Tasks/Sekunde
--- Test B: Project Loom Virtual Threads (1 Task = 1 Virtual Thread) ---
Ergebnisse für [Virtual Threads (Unbeschränkt)]:
Erfolgreiche Tasks: 50000 / 50000
Benötigte Zeit: 382 ms (0.38 Sekunden)
Durchsatz: 130890.05 Tasks/Sekunde
Virtual Threads Systems-Benchmark erfolgreich abgeschlossen.
[!NOTE] Architektonische Erkenntnis In Test A stauen sich 50.000 Tasks hinter 200 Betriebssystem-Threads und benötigen ~13 Sekunden. In Test B unmounten die Virtual Threads bei
sleep()verzögerungsfrei, wodurch alle 50.000 Tasks in 382 Millisekunden abgearbeitet werden – eine 33-fache Durchsatzsteigerung bei minimalem Speicherverbrauch!
Nächste Schritte
Nachdem asynchrone Nebenläufigkeit und Skalierung gemeistert sind, steigen wir tief in die Interna der HotSpot-Engine und des JIT-Compilers ein:
-
Teil 3.2: HotSpot-Interna, JIT & Garbage Collection Tuning
Tiered Compilation (C1/C2 JIT), Escape Analysis, G1GC vs. Generational ZGC und Container-Speicherlimits beherrschen.