Virtual Threads in Java: Project Loom wird Realität
Virtual Threads in Java: Project Loom wird Realität
Es gibt Features, auf die die Java-Community seit Jahren wartet. Virtual Threads gehören zweifellos dazu. Seit September 2022 sind sie als Preview-Feature in Java 19 verfügbar (JEP 425), und mit Java 20 im März 2023 folgte bereits die zweite Preview-Iteration (JEP 436). Nach Jahren der Entwicklung unter dem Projektnamen "Loom" wird das Versprechen leichtgewichtiger Nebenläufigkeit in Java endlich greifbar.
Wir bei encircle360 haben Virtual Threads seit dem Java-19-Release in internen Prototypen ausprobiert und möchten unsere Erkenntnisse teilen. Was steckt dahinter, wann helfen sie -- und wo liegen die Grenzen?
Das Problem: Thread-per-Request stößt an Grenzen
Das klassische Modell für Server-Anwendungen in Java ist einfach: Für jede eingehende Anfrage wird ein Thread zugewiesen, der die Anfrage synchron abarbeitet. In Spring Boot, Jakarta EE oder jedem Servlet-Container ist das der Standard. Ein Request kommt rein, ein Thread aus dem Pool übernimmt ihn, verarbeitet ihn und gibt ihn zurück.
Das funktioniert gut -- bis es das nicht mehr tut. Ein Platform Thread in Java ist ein dünner Wrapper um einen Betriebssystem-Thread. Und Betriebssystem-Threads sind teuer: Jeder konsumiert typischerweise 1 MB Stack-Speicher, das Erzeugen und Zerstören ist aufwändig, und Context Switches zwischen Tausenden von Threads belasten den Kernel-Scheduler erheblich. In der Praxis bedeutet das: Ein typischer Server verkraftet einige Tausend gleichzeitige Threads, bevor der Speicher oder die Scheduling-Kosten zum Engpass werden.
Das war jahrelang kein Problem, weil die meisten Anwendungen nicht Tausende gleichzeitiger Verbindungen brauchten. Doch mit Microservices, die sich gegenseitig aufrufen, und Datenbank-Queries, bei denen der Thread 90 % seiner Zeit wartend verbringt, wird das Thread-Limit zum Flaschenhals für den Durchsatz. Die Anwendung ist nicht CPU-gebunden -- sie wartet nur auf I/O, aber die Threads gehen aus.
Bisherige Lösungen: Reactive und die Kosten der Komplexität
Die Antwort der Java-Welt auf dieses Problem war reaktive Programmierung: Project Reactor, RxJava, Spring WebFlux. Statt einen Thread pro Request zu blockieren, arbeitet man mit nicht-blockierenden Callbacks und Event Loops. Wenige Threads bedienen viele Requests.
Das funktioniert technisch hervorragend. Aber es hat einen hohen Preis: Der gesamte Code muss im reaktiven Stil geschrieben werden. Aus sequentiellen Methoden werden Mono- und Flux-Ketten. Stack Traces werden unlesbar. Debugging wird zur Detektivarbeit. Und jede Library im Stack muss reaktiv kompatibel sein -- ein einziger blockierender Aufruf in der Kette kann das gesamte System lahmlegen.
In unserem Spring-Boot-3-Artikel hatten wir die Frage "WebFlux oder klassischer Stack?" bereits angesprochen. Virtual Threads bieten nun einen dritten Weg: den vertrauten, synchronen Programmierstil beibehalten, aber mit leichtgewichtigen Threads, die Blockieren erlauben, ohne Ressourcen zu verschwenden.
Was sind Virtual Threads?
Virtual Threads sind Threads, die von der JVM verwaltet werden -- nicht vom Betriebssystem. Sie nutzen dasselbe java.lang.Thread-API, das Java-Entwickler seit 1996 kennen. Code, der mit Platform Threads funktioniert, funktioniert auch mit Virtual Threads. Der entscheidende Unterschied liegt unter der Haube.
Wenn ein Virtual Thread auf einen blockierenden Aufruf trifft -- einen Datenbank-Query, einen HTTP-Request, ein Thread.sleep() -- wird er nicht am Betriebssystem-Thread festgehalten. Stattdessen wird er von seinem Carrier Thread abgehängt (unmounted), und der Carrier Thread kann sofort einen anderen Virtual Thread ausführen. Sobald die blockierende Operation abgeschlossen ist, wird der Virtual Thread auf einem verfügbaren Carrier Thread wieder aufgesetzt (mounted) und setzt seine Arbeit fort.
Die Carrier Threads sind ein Pool von Platform Threads -- standardmäßig ein ForkJoinPool, dessen Größe der Anzahl der CPU-Kerne entspricht. Ein Server mit 8 Kernen nutzt also typischerweise 8 Carrier Threads, die aber Hunderttausende oder sogar Millionen von Virtual Threads bedienen können.
Der Speicher-Overhead eines Virtual Threads liegt bei wenigen Kilobyte statt dem typischen Megabyte eines Platform Threads. Erzeugen und Vernichten sind fast kostenfrei. Das ändert die Grundannahmen für Concurrency in Java fundamental.
Virtual Threads in der Praxis
Das Erstellen eines Virtual Threads ist denkbar einfach:
// Einzelnen Virtual Thread starten
Thread.startVirtualThread(() -> {
System.out.println("Hallo aus einem Virtual Thread!");
System.out.println("Thread: " + Thread.currentThread());
});
// Oder über den Builder
Thread vThread = Thread.ofVirtual()
.name("mein-virtueller-thread")
.start(() -> doSomeWork());
Für Server-Anwendungen ist der neue Executor die praxisrelevantere API:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// Jeder Task bekommt seinen eigenen Virtual Thread
for (int i = 0; i < 100_000; i++) {
executor.submit(() -> {
// Simulierter I/O-Aufruf
Thread.sleep(Duration.ofSeconds(1));
return fetchDataFromDatabase();
});
}
}
// Der try-with-resources-Block wartet, bis alle Tasks abgeschlossen sind
Dieses Beispiel erzeugt 100.000 Virtual Threads. Jeder schläft eine Sekunde und führt dann einen Datenbank-Aufruf aus. Mit Platform Threads wäre das undenkbar -- 100.000 OS-Threads würden allein 100 GB Speicher für die Stacks benötigen. Mit Virtual Threads belegt das vielleicht einige hundert Megabyte.
Zum Vergleich der gleiche Ansatz mit Platform Threads:
// Platform Threads -- funktioniert nur mit kleinem Pool
try (var executor = Executors.newFixedThreadPool(200)) {
for (int i = 0; i < 100_000; i++) {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return fetchDataFromDatabase();
});
}
}
// 100.000 Tasks durch 200 Threads: ~500 Sekunden statt ~1 Sekunde
Mit 200 Platform Threads dauert die Verarbeitung von 100.000 Tasks, die jeweils eine Sekunde blockieren, rund 500 Sekunden. Mit Virtual Threads -- bei denen jeder Task seinen eigenen Thread bekommt und blockierendes Warten den Carrier Thread freigibt -- sind es wenige Sekunden. Der Durchsatz bei I/O-lastigen Workloads steigt dramatisch.
Wann Virtual Threads helfen -- und wann nicht
Virtual Threads sind kein Allheilmittel. Sie glänzen bei I/O-gebundenen Workloads: HTTP-Requests, Datenbank-Queries, Dateizugriffe, Message-Queue-Interaktionen. Überall dort, wo Threads die meiste Zeit wartend verbringen, können Virtual Threads den Durchsatz vervielfachen, weil die Carrier Threads nie untätig sind.
Bei CPU-gebundenen Workloads bringen Virtual Threads keinen Vorteil. Wenn ein Thread durchgehend rechnet, gibt es nichts zum Unmounten. Ein CPU-intensiver Algorithmus läuft auf einem Virtual Thread genauso schnell (oder langsam) wie auf einem Platform Thread. Die Anzahl der gleichzeitig rechnenden Threads ist ohnehin durch die CPU-Kerne begrenzt.
Stolpersteine und Einschränkungen
Pinning mit synchronized
Die aktuell wichtigste Einschränkung: Wenn ein Virtual Thread innerhalb eines synchronized-Blocks blockiert, kann er nicht von seinem Carrier Thread abgehängt werden. Er ist "gepinnt" -- der Carrier Thread ist blockiert und kann keine anderen Virtual Threads bedienen. Das untergräbt den gesamten Vorteil.
Die Lösung ist, synchronized durch ReentrantLock zu ersetzen:
// Problematisch: Virtual Thread wird gepinnt
synchronized (lock) {
resultSet = statement.executeQuery(); // Blockiert -- Carrier ist gepinnt
}
// Besser: ReentrantLock erlaubt Unmounting
private final ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
resultSet = statement.executeQuery(); // Blockiert -- Carrier wird freigegeben
} finally {
lock.unlock();
}
Das betrifft nicht nur eigenen Code, sondern auch Libraries. JDBC-Treiber, Connection Pools und andere Infrastruktur-Libraries verwenden intern oft synchronized. Hier muss das Ökosystem nachziehen -- und das passiert bereits.
ThreadLocal-Variablen
ThreadLocal funktioniert mit Virtual Threads, aber die Semantik ändert sich. Bei Platform Threads hat man einige Hundert Threads, und ThreadLocal-Speicher ist überschaubar. Bei Millionen von Virtual Threads kann der Speicherverbrauch durch ThreadLocals explodieren. Das JDK-Team arbeitet an Scoped Values (JEP 429) als Alternative, die aber ebenfalls noch im Preview-Status sind.
Thread-Pools werden zum Anti-Pattern
Ein subtiler, aber wichtiger Punkt: Thread-Pools ergeben mit Virtual Threads keinen Sinn mehr. Ein Pool existiert, um teure Ressourcen (OS-Threads) wiederzuverwenden. Virtual Threads sind so billig, dass man für jeden Task einen neuen erstellen sollte. Executors.newVirtualThreadPerTaskExecutor() ist bewusst so benannt -- ein Thread pro Task, kein Pool.
Virtual Threads vs. Reactive Programming
Die Frage liegt nahe: Brauchen wir noch Spring WebFlux, wenn es Virtual Threads gibt? Die Antwort ist differenziert.
Virtual Threads lösen das gleiche Grundproblem -- effizienter Umgang mit I/O-Wait -- aber mit dem vertrauten synchronen Programmiermodell. Für die große Mehrheit der Anwendungen, die heute reaktiv geschrieben werden müssten, um skalierfähig zu sein, werden Virtual Threads die einfachere und wartbarere Lösung sein.
Reaktive Programmierung hat aber weiterhin ihre Berechtigung: Backpressure, Stream-Verarbeitung und das reaktive Programmiermodell selbst bieten Vorteile, die über reine Thread-Effizienz hinausgehen. Wer reaktive Streams braucht, wird weiterhin auf Reactor oder RxJava setzen. Aber wer bisher nur deshalb reaktiv programmiert hat, weil Thread-Pools nicht groß genug waren, bekommt mit Virtual Threads eine deutlich angenehmere Alternative.
Preview heißt: noch nicht in Produktion
Ein wichtiger Hinweis: Virtual Threads sind in Java 19 und 20 ein Preview-Feature. Das bedeutet, man muss sie explizit mit --enable-preview aktivieren, und Oracle behält sich vor, die API zwischen Preview-Iterationen zu ändern. Für Produktionssysteme empfehlen wir daher, noch zu warten.
Die gute Nachricht: Java 21 erscheint im September 2023 als nächstes LTS-Release, und alles deutet darauf hin, dass Virtual Threads dort ihren finalen Status erreichen werden. Das wäre ein gewaltiger Schritt -- das erste LTS-Release mit Virtual Threads als vollwertigem Feature. In Kombination mit den Neuerungen, die wir in unserem Java-17-Artikel vorgestellt haben -- Sealed Classes, Records, Pattern Matching -- wird Java 21 ein Release, das die Sprache grundlegend modernisiert.
Wie wir uns vorbereiten
Wir bei encircle360 bereiten uns bereits auf den Umstieg vor:
synchronizeddurchReentrantLockersetzen, wo blockierende I/O-Operationen innerhalb des kritischen Abschnitts stattfinden- ThreadLocal-Nutzung prüfen und wo möglich reduzieren
- Library-Kompatibilität evaluieren -- insbesondere JDBC-Treiber und Connection Pools
- Prototypen bauen mit
--enable-previewauf Java 20, um ein Gefühl für das Verhalten unter Last zu bekommen - Java 21 LTS im September einplanen als Zielplattform für den produktiven Einsatz
Virtual Threads ändern nicht, wie wir Java-Code schreiben -- sie ändern, wie die JVM ihn ausführt. Und genau darin liegt ihre Eleganz: Bestehender synchroner Code wird skalierfähiger, ohne dass eine einzige Zeile geändert werden muss. Wir sind gespannt auf September.
Geschrieben von
Patrick HütterGründer & Software-Architekt
Softwarearchitekt, Ingenieur und Unternehmer. Patrick baut seit über einem Jahrzehnt Produkte und Plattformen — von Enterprise-Backends über Cloud-Native-Infrastruktur bis hin zu KI-gestützten Anwendungen. Als Gründer von encircle360 verbindet er technische Tiefe mit unternehmerischem Denken und treibt Open-Source-Projekte voran, die echten Impact haben.
Das könnte dich auch interessieren
Vom Manifest zur Produktion: Wie ADL, A2A und das Inference Gateway die Agenten-Infrastruktur revolutionieren
05.07.2026 · 12 Min. Lesezeit
Agent-Orchestrierung mit Java: Wie Sie LLM-Agenten produktionsreif in die JVM bringen
04.07.2026 · 6 Min. Lesezeit
Spring AI: Wie Java-Entwickler endlich KI-Features ohne Umwege integrieren
25.03.2026 · 5 Min. Lesezeit