Java Concurrency Cheat Sheet
Updated for Java 25 — includes Virtual Threads, Scoped Values (JEP 506) & Structured Concurrency (JEP 505, Preview).
Virtual Threads (Java 21+, JEP 444)
Leichtgewichtige Threads, die von der JVM verwaltet werden — Millionen gleichzeitig möglich.
// Thread starten
Thread.ofVirtual().start(() -> {
System.out.println("Hello from virtual thread!");
});
// Mit Name
Thread.ofVirtual().name("worker-", 0).start(task);
// Mit Executor (empfohlener Weg)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i ->
executor.submit(() -> fetchUrl("https://example.com/" + i))
);
}
// executor.close() wartet auf alle Tasks — try-with-resources!
// Prüfen ob Virtual Thread
Thread.currentThread().isVirtual() // true/false
// Factory für Virtual Threads
ThreadFactory factory = Thread.ofVirtual().name("pool-", 0).factory();
Wann Virtual Threads verwenden?
Gut geeignet: NICHT geeignet:
I/O-bound Tasks (HTTP, DB) CPU-intensive Berechnungen
Viele gleichzeitige Verbindungen Wenige lang laufende Tasks
Request-per-Thread Server Synchronized-heavy Code (vor Java 24)
Pinning vermieden (Java 24+, JEP 491)
// Vor Java 24: synchronized blockierte den Carrier-Thread
// Ab Java 24: Virtual Threads können auch in synchronized unmounten
synchronized (lock) {
// I/O-Operation — ab Java 24 kein Pinning mehr!
var data = httpClient.send(request, bodyHandler);
}
Scoped Values (Java 25, JEP 506)
Sichere, performante Alternative zu ThreadLocal — besonders für Virtual Threads.
// Deklarieren (immer static final)
private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
// Binden und ausführen
ScopedValue.where(CURRENT_USER, user).run(() -> {
handleRequest(); // und alle aufgerufenen Methoden haben Zugriff
});
// Wert lesen (in handleRequest oder tieferem Aufruf)
User user = CURRENT_USER.get(); // wirft NoSuchElementException wenn ungebunden
User user = CURRENT_USER.orElse(null); // default wenn ungebunden
boolean bound = CURRENT_USER.isBound(); // prüfen
// Mit Rückgabewert
String result = ScopedValue.where(CURRENT_USER, user).call(() -> {
return processRequest();
});
// Mehrere Werte binden
ScopedValue.where(CURRENT_USER, user)
.where(TRACE_ID, traceId)
.run(() -> handleRequest());
// Rebinding in verschachteltem Scope
ScopedValue.where(CURRENT_USER, user).run(() -> {
// CURRENT_USER.get() → user
ScopedValue.where(CURRENT_USER, adminUser).run(() -> {
// CURRENT_USER.get() → adminUser
});
// CURRENT_USER.get() → user (wiederhergestellt)
});
ScopedValue vs. ThreadLocal
ScopedValue ThreadLocal
Immutable innerhalb Scope Jederzeit mutierbar
Automatisches Cleanup Manuelles remove() nötig (Memory Leak!)
Vererbung in StructuredTaskScope InheritableThreadLocal (kopiert Daten)
Sehr performant mit VTs Overhead bei vielen VTs
Klar begrenzter Scope Unbegrenzter Lebenszyklus
Structured Concurrency (Java 25, JEP 505 — Preview)
Macht nebenläufige Tasks strukturiert — wie verschachtelte Codeblöcke.
// Preview-Feature: --enable-preview nötig
// Einfaches Beispiel: zwei Tasks parallel ausführen
try (var scope = StructuredTaskScope.open()) {
Subtask<String> user = scope.fork(() -> fetchUser(id));
Subtask<String> order = scope.fork(() -> fetchOrder(id));
scope.join(); // warte auf alle
return new Response(user.get(), order.get());
}
// scope.close() cancelled alle noch laufenden Tasks automatisch!
// Mit Joiner: Erstes Ergebnis gewinnt (Race-Pattern)
try (var scope = StructuredTaskScope.open(Joiner.anySuccessfulResultOrThrow())) {
scope.fork(() -> fetchFromMirror1(url));
scope.fork(() -> fetchFromMirror2(url));
scope.fork(() -> fetchFromMirror3(url));
var result = scope.join(); // gibt erstes erfolgreiches Ergebnis zurück
// alle anderen Tasks werden automatisch abgebrochen
}
// Mit Joiner: Alle müssen erfolgreich sein
try (var scope = StructuredTaskScope.open(Joiner.allSuccessfulOrThrow())) {
scope.fork(() -> validatePayment(order));
scope.fork(() -> checkInventory(order));
scope.fork(() -> verifyAddress(order));
scope.join(); // wirft bei erstem Fehler, bricht andere ab
}
Subtask States
Subtask<String> task = scope.fork(() -> compute());
scope.join();
task.state() // UNAVAILABLE → RUNNING → SUCCESS | FAILED
task.get() // Ergebnis (nur bei SUCCESS, sonst IllegalStateException)
task.exception() // Exception (nur bei FAILED)
Klassische Thread-Synchronisation
synchronized & Locks
// synchronized Block
synchronized (lock) {
sharedState.update();
}
// ReentrantLock — flexibler als synchronized
var lock = new ReentrantLock();
lock.lock();
try {
sharedState.update();
} finally {
lock.unlock();
}
// Trylock mit Timeout
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try { /* ... */ } finally { lock.unlock(); }
}
// ReadWriteLock — mehrere Reader, ein Writer
var rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock(); // mehrere gleichzeitig
rwLock.writeLock().lock(); // exklusiv
// StampedLock — optimistisches Lesen (Java 8+)
var sl = new StampedLock();
long stamp = sl.tryOptimisticRead();
int value = sharedValue;
if (!sl.validate(stamp)) {
stamp = sl.readLock();
try { value = sharedValue; } finally { sl.unlockRead(stamp); }
}
Atomic Classes
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // ++count
count.getAndIncrement(); // count++
count.compareAndSet(expected, newValue); // CAS
count.updateAndGet(x -> x * 2); // atomare Transformation
count.accumulateAndGet(5, Integer::sum); // atomar akkumulieren
// Für High-Contention: LongAdder/LongAccumulator (Java 8+)
LongAdder adder = new LongAdder();
adder.increment();
adder.sum(); // Ergebnis lesen
// AtomicReference für Objekte
AtomicReference<User> ref = new AtomicReference<>(user);
ref.compareAndSet(oldUser, newUser);
Concurrent Collections
// Map
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.compute("key", (k, v) -> v == null ? 1 : v + 1); // atomar
map.merge("key", 1, Integer::sum); // atomar
map.putIfAbsent("key", defaultValue);
map.computeIfAbsent("key", k -> expensiveCompute(k));
// Queue
ConcurrentLinkedQueue<Task> queue = new ConcurrentLinkedQueue<>();
BlockingQueue<Task> bq = new LinkedBlockingQueue<>(capacity);
bq.put(task); // blockiert wenn voll
bq.take(); // blockiert wenn leer
bq.offer(task, 1, TimeUnit.SECONDS); // mit Timeout
// List (Copy-on-Write — gut für viel Lesen, wenig Schreiben)
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
// Set
ConcurrentHashMap.newKeySet()
CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();
CompletableFuture
// Async starten
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> fetchData());
// Verketten
cf.thenApply(String::toUpperCase) // transformieren
.thenAccept(System.out::println) // konsumieren
.thenRun(() -> log("done")); // danach ausführen
// Async Verkettung
cf.thenApplyAsync(this::process) // in anderem Thread
.thenComposeAsync(this::fetchMore); // flatMap-Äquivalent
// Mehrere kombinieren
CompletableFuture.allOf(cf1, cf2, cf3).join(); // warte auf alle
CompletableFuture.anyOf(cf1, cf2, cf3).join(); // warte auf ersten
// Fehlerbehandlung
cf.exceptionally(ex -> "fallback")
.handle((result, ex) -> ex != null ? "error" : result)
.whenComplete((result, ex) -> log(result, ex));
// Timeout (Java 9+)
cf.orTimeout(5, TimeUnit.SECONDS)
.completeOnTimeout("default", 5, TimeUnit.SECONDS);
Executors & Thread Pools
// Virtual Threads (Java 21+ — bevorzugt für I/O!)
Executors.newVirtualThreadPerTaskExecutor()
// Klassische Thread Pools
Executors.newFixedThreadPool(nThreads) // feste Größe
Executors.newCachedThreadPool() // dynamisch
Executors.newSingleThreadExecutor() // ein Thread
Executors.newScheduledThreadPool(coreSize) // zeitgesteuert
// Scheduled Execution
var scheduler = Executors.newScheduledThreadPool(1);
scheduler.schedule(task, 5, TimeUnit.SECONDS); // einmal nach Delay
scheduler.scheduleAtFixedRate(task, 0, 1, TimeUnit.SECONDS); // wiederholt
scheduler.scheduleWithFixedDelay(task, 0, 1, TimeUnit.SECONDS);
// Immer shutdown aufrufen!
executor.shutdown();
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
Countdown & Barriers
// CountDownLatch — einmalig, Threads warten auf Countdown
CountDownLatch latch = new CountDownLatch(3);
latch.countDown(); // in Worker-Threads
latch.await(); // wartet bis count == 0
// CyclicBarrier — wiederverwendbar, Threads warten aufeinander
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("Phase done"));
barrier.await(); // jeder Thread wartet bis alle da sind
// Semaphore — begrenzte Anzahl gleichzeitiger Zugriffe
Semaphore semaphore = new Semaphore(5);
semaphore.acquire();
try { accessResource(); } finally { semaphore.release(); }
// Phaser (Java 7+) — flexibler als CyclicBarrier
Phaser phaser = new Phaser(1);
phaser.register(); // Teilnehmer hinzufügen
phaser.arriveAndAwaitAdvance();
phaser.arriveAndDeregister();
Quick-Reference: Was wofür?
Aufgabe Lösung
──────────────────────────────── ────────────────────────────────
Viele I/O Tasks parallel Virtual Threads + Executor
Implizite Kontextdaten ScopedValue (statt ThreadLocal)
Fan-out/Fan-in parallel StructuredTaskScope (Preview)
Einfacher Shared Counter AtomicInteger / LongAdder
Thread-sichere Map ConcurrentHashMap
Producer-Consumer BlockingQueue
Async Pipeline CompletableFuture
Exklusiver Zugriff ReentrantLock / synchronized
Viele Reader, wenig Writer ReadWriteLock / StampedLock
N Tasks abwarten CountDownLatch
Threads synchronisieren CyclicBarrier
Zugriff begrenzen Semaphore