4 Minuten
Virtual Threads in der Praxis: Wann sie sich lohnen und wann nicht
Einführung
Virtual Threads (Project Loom) sind seit Java 21 stabil verfügbar und mit Java 25 LTS sowie Java 26 wurden weitere Verbesserungen eingeführt. Doch obwohl die Technologie inzwischen fast drei Jahre auf dem Markt ist, herrscht oft noch Unsicherheit, wann Virtual Threads wirklich Sinn machen und wann sie eher Overhead produzieren. Hier eine praktische Einschätzung.
Was Virtual Threads sind
Virtual Threads sind leichtgewichtige Threads, die von der JVM verwaltet werden — nicht vom Betriebssystem. Während ein Plattform-Thread (OS-Thread) typischerweise 1 MB Stack-Speicher reserviert, kostet ein Virtual Thread nur wenige Bytes. Das bedeutet: du kannst hunderttausende Virtual Threads erstellen, ohne dass das System ins Schwitzen kommt.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 100_000).forEach(i -> {
executor.submit(() -> fetchUrl("https://example.com/" + i));
});
}
100.000 gleichzeitige Tasks mit einem Executor, der für jeden Task einen eigenen Virtual Thread startet — das funktioniert problemlos.
Wo Virtual Threads Sinn machen
I/O-gebundene Workloads
Das ist das klassische Einsatzgebiet. Jeder Task, der auf externe Ressourcen wartet — Datenbankabfragen, HTTP-Calls, Datei-Operationen — profitiert massiv von Virtual Threads.
// Beispiel: Parallele API-Calls
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var user = scope.fork(() -> httpClient.send(userRequest, BodyHandlers.ofString()));
var orders = scope.fork(() -> httpClient.send(ordersRequest, BodyHandlers.ofString()));
scope.join();
scope.throwIfFailed();
}
Während ein Virtual Thread auf den HTTP-Response wartet, gibt er den Carrier-Thread frei. Andere Virtual Threads können auf demselben Carrier weiterarbeiten. Das skaliert deutlich besser als ein Thread-Pool fester Größe.
Microservices und API-Gateways
Spring Boot 3.2+ unterstützt Virtual Threads out-of-the-box. Ein einfacher Konfigurationseintrag reicht:
spring:
threads:
virtual:
enabled: true
Für typische Spring Boot-Anwendungen, die viele parallele Datenbank- und HTTP-Calls ausführen, bringt das messbare Verbesserungen beim Durchsatz — besonders unter Last.
Alte blocking Codebases
Hier liegen die größten Vorteile. Legacy-Code, der blocking I/O macht und bisher mit Thread-Pools begrenzt wurde, kann mit Virtual Threads hochskaliert werden, ohne den Code umzuschreiben. Sync-Code bleibt sync-Code, aber die Skalierbarkeit ändert sich grundlegend.
Wo Virtual Threads nicht Sinn machen
CPU-gebundene Workloads
Virtual Threads helfen nicht bei Berechnungen. Wenn du einen Task hast, der primär CPU-Zeit verbraucht (Bildverarbeitung, Kryptografie, komplexe Berechnungen), dann limitiert die Anzahl der CPU-Kerne den Durchsatz — nicht die Thread-Verwaltung. Hier ist ForkJoinPool oder parallelStream die bessere Wahl.
Synchronisation mit synchronized
Das ist eine wichtige Einschränkung: Virtual Threads, die auf einem synchronized-Block blockieren, pin den Carrier-Thread. Das bedeutet, der OS-Thread kann während der Sperre nicht für andere Virtual Threads verwendet werden. Bei hoher Konkurrenz skaliert das schlecht.
// Problematisch: synchronized pinnt den Carrier
public synchronized void addToCache(String key, String value) {
cache.put(key, value);
}
// Besser: ReentrantLock
private final ReentrantLock lock = new ReentrantLock();
public void addToCache(String key, String value) {
lock.lock();
try {
cache.put(key, value);
} finally {
lock.unlock();
}
}
Ab Java 24 gibt es zwar Verbesserungen beim Pinning-Verhalten, aber das Grundproblem bleibt: ReentrantLock oder java.util.concurrent-Konstrukte sind Virtual-Thread-freundlicher als synchronized.
JNI und native Code
Aufrufe in nativen Code über JNI können ebenfalls zum Pinning führen, da die JVM den Zustand des Carrier-Threads nicht auf einen anderen Thread umhängen kann. Wer viel JNI nutzt, sollte Virtual Threads in diesem Bereich mit Vorsicht einsetzen.
Verbesserungen in Java 25 und 26
Mit Java 25 wurde das Thread-Pinning weiter reduziert. Virtual Threads unmounten sich jetzt auch beim Warten auf Class-Initializer, was in Java 26 nochmal verbessert wurde. Das bedeutet weniger Pinning-Situationen im produktiven Betrieb.
Zudem wurde das Zusammenspiel mit Structured Concurrency (JEP 525, 6. Preview in Java 26) verbessert. Die Kombination aus Virtual Threads und Structured Concurrency ermöglicht es, parallele Tasks als logische Einheit zu behandeln — mit automatischer Fehlerbehandlung und Abbruch.
Praxis-Tipps
- Virtual Threads nicht in Arrays oder Collections zwischenspeichern. Sie sind disposable, nicht wiederverwendbar.
- Thread-Local-Variablen sparsam einsetzen. Bei 100.000 Virtual Threads multipliziert sich der Overhead.
- Monitoring anpassen: herkömmliche Thread-Dumps zeigen Virtual Threads nicht sinnvoll.
jcmd <pid> Thread.dump_to_fileunterstützt ab Java 21 Virtual Threads nativ. - Lasttests machen: Das Verhalten kann sich je nach GC und Hardware unterscheiden.
Fazit
Virtual Threads sind für I/O-gebundene Java-Anwendungen ein wichtiges Werkzeug. Sie reduzieren die Komplexität bei der Parallelisierung und machen Thread-Management in vielen Fällen überflüssig. Für CPU-intensive Tasks oder Codebases mit viel synchronized und JNI sind sie hingegen nicht die richtige Wahl.
Wer Spring Boot nutzt, kann mit minimalem Aufwand von Virtual Threads profitieren. Ein Upgrade auf Java 25 LTS und spring.threads.virtual.enabled=true reicht aus, um die Vorteile zu nutzen.