Über unsMediaKontaktImpressum
Andreas Blüml 04. März 2026

Virtuelle Threads in Java 21 – ein Meilenstein für Nebenläufigkeit

Mit Java 21 hält eine der bedeutendsten Neuerungen der letzten Jahre Einzug in die Java-Welt: virtuelle Threads. Sie revolutionieren die Art und Weise, wie parallele Verarbeitung in Java-Anwendungen umgesetzt wird, und versprechen eine drastische Vereinfachung sowie Leistungssteigerung bei der Arbeit mit vielen gleichzeitigen Aufgaben.

Traditionell basierte die Nebenläufigkeit in Java auf Plattform-Threads, die direkt vom Betriebssystem verwaltet werden. Diese nativen Threads sind jedoch schwergewichtig – jeder benötigt einen eigenen Stack-Speicher (typischerweise rund 1 MB) und erzeugt zusätzlichen Verwaltungsaufwand im Betriebssystem-Kernel. Das führt schnell zu Skalierungsgrenzen: Anwendungen, die zehntausende gleichzeitige Aufgaben verarbeiten müssen, geraten mit klassischen Threads an ihre Leistungsgrenze.

Genau hier setzt das Projekt Loom von Oracle an. Es wurde mit Java 21 offiziell eingeführt und bringt mit virtuellen Threads (Thread.ofVirtual()) eine völlig neue, leichtgewichtige Thread-Implementierung. Virtuelle Threads eliminieren die bisherigen Einschränkungen nativer Threads, indem sie von der JVM statt vom Betriebssystem verwaltet werden. Das Ergebnis:

  • schnellere Erstellung von Threads ohne teure Systemaufrufe
  • deutlich geringerer Speicherverbrauch
  • bessere Skalierbarkeit für hochgradig nebenläufige Anwendungen

Mit virtuellen Threads wird Nebenläufigkeit in Java endlich so einfach und effizient, wie sie schon immer gedacht war.

Warum virtuelle Threads so effizient sind

Ein entscheidender Unterschied zwischen virtuellen Threads und herkömmlichen Plattform-Threads liegt in ihrer Verwaltung. Während klassische Threads direkt vom Betriebssystem gesteuert werden, übernimmt bei virtuellen Threads die Java Virtual Machine (JVM) selbst die Kontrolle. Dadurch entfallen viele kostspielige Operationen, die bei nativen Threads notwendig sind – etwa Systemaufrufe zur Thread-Erstellung oder das Zuweisen großer Speicherbereiche für den Stack.

Virtuelle Threads sind deshalb extrem leichtgewichtig: Sie können in großer Zahl erzeugt und wieder verworfen werden, ohne dass die Performance darunter leidet. Wo früher wenige Tausend Threads das Limit darstellten, sind mit virtuellen Threads Millionen paralleler Aufgaben möglich. Besonders profitieren davon Anwendungen, die viele gleichzeitige, aber nicht CPU-intensive Tätigkeiten ausführen – etwa Server-Anwendungen, die eine Vielzahl von Datenbank- oder Netzwerk-Anfragen zeitgleich verarbeiten.

Effizientere Parallelverarbeitung durch intelligente Ressourcennutzung

Der große Vorteil virtueller Threads zeigt sich vor allem bei der Parallelverarbeitung:

Virtuelle Threads blockieren keine Betriebssystem-Threads, wenn sie auf langsame I/O-Operationen warten – zum Beispiel beim Zugriff auf eine Datenbank oder bei Netzwerkkommunikation. Stattdessen kann die JVM den zugrunde liegenden Träger-Thread ("Carrier Thread") freigeben, sodass andere Aufgaben ausgeführt werden können, während der virtuelle Thread im Hintergrund auf seine Antwort wartet.

Dieses kooperative Scheduling sorgt für eine effizientere Nutzung der verfügbaren Ressourcen. Die CPU bleibt aktiv, anstatt untätig auf I/O zu warten, und die Anwendung kann deutlich besser skalieren, insbesondere in stark parallelisierten Szenarien.

Das Ergebnis: weniger Overhead, mehr Leistung und eine wesentlich einfachere Handhabung von Nebenläufigkeit – ganz ohne komplexe asynchrone Programmierung.

Wie virtuelle Threads wirklich ausgeführt werden

Virtuelle Threads sind – anders als herkömmliche Threads – keine echten Betriebssystem-Threads. Dennoch müssen sie natürlich irgendwo ausgeführt werden. Die eigentliche Arbeit übernehmen dabei sogenannte Carrier Threads.

Was sind Carrier Threads?

Carrier Threads sind ganz normale native Threads (also klassische OS Threads), die von der Java Virtual Machine (JVM) genutzt werden, um virtuelle Threads tatsächlich auszuführen. Man kann sie sich als Träger vorstellen, auf denen virtuelle Threads für ihre aktive Laufzeit "mitfahren".

Dabei gilt: Virtuelle Threads laufen nicht direkt auf der CPU, sondern werden von der JVM dynamisch einem Carrier Thread zugewiesen.

Mehrere virtuelle Threads können sich denselben Carrier Thread teilen – ähnlich wie Goroutines in Go. Wobei je Carrier Thread nur ein virtueller Thread aktiv sein kann. Ebenso, wie auch die CPU je Kern Aufgaben sequenziell verarbeitet.

Die Anzahl der Carrier Threads ist begrenzt und orientiert sich standardmäßig an der Anzahl der verfügbaren CPU-Kerne (Runtime.getRuntime().availableProcessors()). Dies kann aber per JVM-Parameter konfiguriert werden.

Ein Beispiel zur Veranschaulichung

Stell dir vor, du startest 1.000.000 virtuelle Threads.

Die JVM legt auf einem System mit 8 CPU-Kernen etwa 8 Carrier Threads an. Diese nativen Threads führen abwechselnd jeweils aktive virtuelle Threads aus. Sobald ein virtueller Thread blockiert – etwa durch eine Datenbankabfrage oder einen Netzwerkaufruf – wird er pausiert und der Carrier Thread übernimmt eine andere Aufgabe.

Das Ergebnis:

Obwohl in deiner Anwendung Millionen virtueller Threads gleichzeitig existieren, sind tatsächlich nur wenige Dutzend native Threads aktiv. Dieses Prinzip macht virtuelle Threads so leichtgewichtig, skalierbar und ressourcenschonend.

Durch diese intelligente Trennung zwischen virtuellen Threads (logische Einheiten) und Carrier Threads (physische Ausführungsumgebung) kann Java 21 eine enorme Parallelität erreichen – ohne die typischen Nachteile klassischer Multithreading-Modelle.

Wichtiger Unterschied – gleiche Arbeit, andere Verwaltung

Bei aller Effizienz darf man jedoch nicht vergessen: Die Arbeit selbst bleibt dieselbe.

Ein Thread, der Daten verarbeitet, führt genau dieselben Berechnungen aus – unabhängig davon, ob er ein virtueller oder ein Plattform-Thread ist. Der entscheidende Unterschied liegt nicht in der Rechenleistung, sondern in der Verwaltung der Ressourcen.

  • Bei Plattform-Threads übernimmt das Betriebssystem die Verwaltung und Zuweisung der Rechenzeit.
  • Bei virtuellen Threads wird diese Aufgabe von der JVM selbst gesteuert, die effizienter mit blockierenden Operationen umgehen kann.

Das bedeutet: Virtuelle Threads machen deine Anwendung nicht automatisch schneller, aber sie ermöglichen, viel mehr an gleichzeitigen Aufgaben mit derselben Hardware auszuführen – und das mit deutlich weniger Verwaltungsaufwand.

Wie das Scheduling virtueller Threads funktioniert

Damit virtuelle Threads effizient arbeiten können, braucht es ein intelligentes Scheduling-System innerhalb der JVM. Virtuelle Threads werden wie beschrieben nicht dauerhaft an einen bestimmten CPU Thread gebunden, sondern laufen zeitweise auf den Carrier Threads.

Anstatt für jeden virtuellen Thread einen eigenen nativen Thread zu erstellen, nutzt die JVM einen Pool aus wenigen Carrier Threads, um Millionen virtueller Threads abwechselnd auszuführen. Dieses System wird vom ForkJoinPool gesteuert – einem hochoptimierten Thread Pool, in dem jeder ("Carrier") Thread seine Aufgaben verwaltet und bei Leerlauf Aufgaben von anderen übernimmt ("Work Stealing").

Die Größe dieses Pools entspricht standardmäßig der Anzahl der verfügbaren CPU-Kerne, kann aber bei Bedarf über die JVM-Option jdk.virtualThreadScheduler.parallelism=<n> 

Der interne Ablauf im Detail:

Schritt 1: Virtueller Thread wird gestartet

Wenn Thread.startVirtualThread(runnable) aufgerufen wird, erzeugt die JVM einen neuen virtuellen Thread, der zunächst nur im Speicher existiert. Er wird einem verfügbaren Carrier Thread aus dem ForkJoinPool zugewiesen – aber erst, wenn er tatsächlich ausgeführt werden soll.

Schritt 2: Virtueller Thread läuft auf einem Carrier Thread

Der Carrier Thread startet die Ausführung des Codes des virtuellen Threads. Solange der Code aktiv läuft, bleibt der virtuelle Thread auf diesem Carrier Thread "gemountet".

Schritt 3: Blockiert der virtuelle Thread? (Stack Parking!)

Trifft der virtuelle Thread auf eine blockierende Operation – etwa bei Thread.sleep(), socket.read() oder einer Datenbankabfrage – passiert Magie:

  1. Der Stack des virtuellen Threads wird von der JVM in den Heap verschoben (dieser Vorgang heißt Stack Parking).
  2. Der Carrier Thread wird sofort freigegeben und kann einen anderen virtuellen Thread ausführen.
  3. Sobald die blockierende Operation abgeschlossen ist, wird der virtuelle Thread wieder einem freien Carrier Thread zugewiesen (unmount) und dort fortgesetzt.

Das Ergebnis:

Kein einziger nativer Thread wird blockiert – und trotzdem laufen alle Aufgaben so, als wären sie ganz normale Threads! Auf diese Weise können Hunderttausende oder sogar Millionen virtueller Threads gleichzeitig aktiv sein, die sich nur eine Handvoll Carrier Threads teilen.

Warum dieses Modell so leistungsfähig ist

Blockierende Operationen, die in klassischen Threads die CPU-Zeit verschwenden würden, blockieren hier nur den virtuellen Thread, nicht aber den Carrier Thread. Dadurch können mit einem kleinen Pool von Träger-Threads sehr viele gleichzeitige Aufgaben effizient verarbeitet werden – perfekt für Server-Anwendungen, die auf viele parallele I/O-Vorgänge reagieren müssen.

Ein anschauliches Beispiel:

Drei Aufgaben führen jeweils kurze Rechenoperationen aus und blockieren dann mehrmals auf I/O. Anstatt drei OS Threads zu belegen, verteilt die JVM die Arbeit dynamisch auf einen einzigen Carrier Thread, der immer wieder andere virtuelle Threads ausführt, sobald einer blockiert. Das Ergebnis ist ein effizientes Scheduling, das die CPU optimal auslastet und gleichzeitig die Komplexität für den Entwickler drastisch reduziert.

Technische Umsetzung in Java 21

Mit Java 21 wurde die Arbeit mit virtuellen Threads einfach und intuitiv gestaltet. Entwickler können virtuelle Threads ähnlich wie klassische Threads erstellen – jedoch mit einer neuen, speziell dafür vorgesehenen API. Dadurch lässt sich bestehender Code oft ohne große Anpassungen umstellen.

Erstellen und Starten eines virtuellen Threads

Die direkteste Möglichkeit, einen virtuellen Thread zu erzeugen und zu starten, ist über die neue ThreadFactory-API:

Thread vt = Thread.ofVirtual().start(() -> {
    System.out.println("Virtueller Thread: " + Thread.currentThread());
});

Dieser Code startet einen virtuellen Thread, der den angegebenen Codeblock ausführt. Von außen verhält sich der Thread wie gewohnt – mit dem Unterschied, dass er intern nicht an einen Betriebssystem-Thread gebunden ist, sondern als virtueller Thread von der JVM verwaltet wird.

Alternative Kurzform: startVirtualThread(). Etwas kompakter gelingt das mit der neuen statischen Methode Thread.startVirtualThread():

Thread vt = Thread.startVirtualThread(() -> {
    System.out.println("Virtueller Thread: " + Thread.currentThread());
});

Diese Variante kombiniert die Erstellung und den Start in einem Schritt und ist ideal für einfache Aufgaben oder Tests.

Virtuelle Threads im ExecutorService: Für Anwendungen, die viele gleichzeitige Aufgaben ausführen müssen, bietet sich der neue ExecutorService auf Basis virtueller Threads an, wenn auch diese Variante mit Vorsicht zu verwenden ist. Der virtuelle Threadpool übernimmt schon viele Aufgaben des ExecutorServices:

ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
executorService.submit(() -> {
    System.out.println("Virtueller Thread: " + Thread.currentThread());
});

Dieser Executor erstellt für jede Aufgabe automatisch einen neuen virtuellen Thread. Am Ende sollte der Executor wie gewohnt beendet werden, um Ressourcen freizugeben:

executorService.shutdown();

Die neuen APIs in Java 21 machen es erstaunlich einfach, von klassischen Threads auf virtuelle umzusteigen. Der Entwickler schreibt nahezu denselben Code – die JVM sorgt im Hintergrund für leichtere Threads, bessere Skalierung und effizientere Ressourcennutzung.

Einstellmöglichkeiten und JVM-Parameter

Die Standardkonfiguration virtueller Threads in Java 21 ist für die meisten Anwendungen optimal gewählt. Dennoch bietet die JVM verschiedene Tuning-Parameter, mit denen sich das Verhalten des virtuellen Thread Schedulers anpassen lässt. Diese Optionen steuern im Allgemeinen, wie viele Carrier Threads genutzt werden oder das Logging.

jdk.virtualThreadScheduler.parallelism

Beispiel: jdk.virtualThreadScheduler.parallelism=64

Dieser Parameter legt die Anzahl der Carrier Threads im Scheduler Pool fest – also die nativen Threads, auf denen virtuelle Threads tatsächlich ausgeführt werden. Der Standardwert entspricht der Anzahl der verfügbaren CPU-Kerne: Runtime.getRuntime().availableProcessors()

In den meisten Fällen ist dieser Standard völlig ausreichend, da mehr Carrier Threads nicht automatisch zu höherer Performance führen. Es könnte hilfreich sein, diesen Wert zu erhöhen, wenn in der Anwendung viele Threads zeitintensive Aufgaben durchführen, ohne zu blockieren, also den Carrier Thread lange belegen. Es könnte auch hilfreich sein, diesen Wert zu erhöhen, wenn die Anwendung in einer beispielsweise Docker-, Podman-, Openshift- etc. Umgebung läuft, in der die Anzahl der CPU-Kerne deutlich reduziert wurde.

jdk.virtualThreadScheduler.minRunnable

Beispiel: jdk.virtualThreadScheduler.minRunnable=8

Dieser Wert definiert, wie viele virtuelle Threads gleichzeitig aktiv ("runnable") sein müssen, bevor der Scheduler zusätzliche Carrier Threads erzeugt.

Der Standardwert ist die größere Zahl aus 1 (damit mindestens ein Carrier Thread immer aktiv sein kann, selbst bei nur einem CPU-Kern) und parallelism / 2 (also die Hälfte der verfügbaren CPU-Kerne).

Dadurch wird sichergestellt, dass der Scheduler effizient arbeitet, ohne unnötig viele Carrier Threads zu starten.

jdk.virtualThreadScheduler.maxPoolSize

Beispiel: jdk.virtualThreadScheduler.maxPoolSize=256

Dieser Parameter legt die maximale Anzahl von Carrier Threads im Pool fest. Der Standardwert beträgt 256 und dient als Sicherheitsgrenze, um ein unbegrenztes Wachstum des Pools zu verhindern. Wichtig: Dieser Wert überschreibt nicht den Parallelism-Parameter – er legt nur eine Obergrenze fest.

Ein häufiger Irrtum ist, diesen Wert zu erhöhen, wenn viele virtuelle Threads genutzt werden. In Wahrheit stellt sich dann die Frage: "Warum brauchen wir so viele parallel aktive Threads?" Virtuelle Threads sind nicht dafür gedacht, CPU-intensive Berechnungen in massiver Parallelität auszuführen. Ihr Vorteil liegt in der Verarbeitung vieler I/O-lastiger Aufgaben, nicht in reiner Rechenleistung. 

jdk.tracePinnedThreads

Beispiel: jdk.tracePinnedThreads=full

Dieser Parameter ermöglicht die Nachverfolgung sogenannter "gepinnter" virtueller Threads – also Threads, die vorübergehend fest an einen bestimmten Carrier Thread gebunden sind.

  • disabled → Nachverfolgung deaktiviert (Standard)
  • short → kurzer Überblick über gepinnte Threads
  • full → detaillierte Protokollierung inklusive Stacktraces

Diese Option ist besonders hilfreich beim Debugging oder bei Performance-Analysen, um zu verstehen, wann und warum virtuelle Threads nicht vom Carrier Thread getrennt werden können.

Die JVM bietet mit diesen Parametern feine Stellschrauben, um das Verhalten virtueller Threads zu optimieren. In der Praxis sind jedoch die Standardwerte in fast allen Fällen ausreichend.

Wer mehr als 256 aktive Carrier Threads benötigt, sollte prüfen, ob die Anwendung CPU- oder I/O-lastig ist – denn für rechenintensive Aufgaben sind virtuelle Threads nicht die richtige Wahl.

Pinned Threads – wenn virtuelle Threads ihre Flexibilität verlieren

Virtuelle Threads verdanken ihre Effizienz der Möglichkeit, jederzeit vom Carrier Thread entkoppelt ("unmounted") und auf einem anderen wieder fortgesetzt zu werden. Doch es gibt Situationen, in denen das nicht möglich ist: Ein virtueller Thread wird dann "gepinnt", also vorübergehend an einen bestimmten Carrier Thread gebunden.

Das bedeutet: Solange der Thread "gepinnt" ist, kann die JVM ihn nicht verschieben oder parken, wodurch ein nativer Thread blockiert bleibt – und damit der zentrale Vorteil der virtuellen Threads teilweise verloren geht.

Ursachen für Pinned Threads

Pinned Threads entstehen immer dann, wenn Code ausgeführt wird, der nicht mit der flexiblen Scheduling-Architektur virtueller Threads kompatibel ist. Typische Ursachen sind:

Synchronized-Blöcke und -Methoden: Wenn ein virtueller Thread innerhalb eines Synchronized-Blocks blockiert, bleibt er so lange an einen Carrier Thread gebunden, bis die Sperre freigegeben wird.

JNI (Java Native Interface): Native Methoden, die länger laufen oder blockieren, verhindern das Entkoppeln des virtuellen Threads. Der Thread bleibt an den zugrunde liegenden Kernel Thread gebunden.

ThreadLocal-Speicher: Der Zugriff auf ThreadLocal-Variablen kann dazu führen, dass virtuelle Threads nicht frei verschoben werden können, da die JVM ihre Zuordnung zu einem bestimmten Thread beibehalten muss.

Auswirkungen von Pinned Threads

Wenn virtuelle Threads gepinnt werden, hat das direkte Auswirkungen auf die Leistungsfähigkeit einer Anwendung:

  • Reduzierte Parallelität → Gepinnte Threads blockieren Carrier Threads und verringern die Skalierbarkeit. Wenn häufig Threads gepinnt werden, kann es sogar dazu kommen, dass alle Carrier Threads blockiert sind und die Anwendung einfach gar nichts mehr macht. Und das passiert, ohne dass sie dies meldet.
  • Höherer Ressourcenverbrauch → Mehr native Threads werden länger gebunden, was Speicher und CPU-Zeit kostet.
  • Erhöhte Latenz → Wenn viele Threads gleichzeitig gepinnt sind, können Aufgaben länger auf Ausführung warten.

Best Practices – Vermeidung von Pinned Threads

Um die Vorteile virtueller Threads voll auszuschöpfen, sollten Entwickler darauf achten, Pinning möglichst zu vermeiden:

  • Kurze oder besser keine Synchronized-Blöcke verwenden! Diese können einfach durch ReentrantLock ersetzt werden.
  • Nicht-blockierende APIs für I/O-Operationen nutzen! Tatsächlich sind viele Dependencies (z.B. JDBC-Treiber) noch nicht für virtuelle Threads optimiert. Eine Analyse zur Laufzeit könnte hier sinnvoll sein.
  • JNI-Aufrufe minimieren oder sicherstellen, dass native Methoden keine langen Blockaden verursachen!
  • Zurückhaltender Einsatz von ThreadLocal-Variablen, insbesondere in stark parallelisierten Anwendungen!

Erkennung von Pinned Threads

Die JVM bietet eine einfache Möglichkeit, Pinning zu erkennen und zu analysieren: den Parameter jdk.tracePinnedThreads=short oder jdk.tracePinnedThreads=full.

Mit dieser Option lässt sich die Nachverfolgung aktivieren. So lässt sich genau feststellen, welcher Code das Pinning verursacht – ein wertvolles Werkzeug für Performance-Optimierungen.

Pinned Threads sind kein Fehler, sondern eine technische Notwendigkeit in bestimmten Situationen. Dennoch sollten sie bewusst minimiert werden, um die Effizienz des virtuellen Thread-Modells nicht zu beeinträchtigen.

Je besser Entwickler die Ursachen verstehen, desto gezielter können sie den Code so gestalten, dass virtuelle Threads ihre volle Flexibilität und Skalierbarkeit entfalten können.

Fazit

Die Vorteile virtueller Threads

Virtuelle Threads bringen mit Java 21 einen enormen Fortschritt in der Art und Weise, wie Nebenläufigkeit in Java umgesetzt wird. Sie sind leichter, schneller und effizienter als herkömmliche Plattform-Threads – ohne dass Entwickler ihre gewohnte Arbeitsweise grundlegend ändern müssen.

Schneller Start und minimale Erzeugungskosten

Das Erstellen eines Plattform-Threads dauert typischerweise rund 1 Millisekunde. Ein virtueller Thread hingegen wird in weniger als 1 Mikrosekunde erzeugt – also über tausendmal schneller. Dadurch ist es erstmals möglich, Millionen von Threads gleichzeitig zu starten, ohne dass dies die Anwendung oder das Betriebssystem überlastet.

Deutlich geringerer Speicherverbrauch

Auch beim Speicherverbrauch zeigen sich virtuelle Threads extrem effizient: Ein Plattform-Thread reserviert standardmäßig etwa 1 MB Stack-Speicher (wobei je nach Betriebssystem 32–64 KB initial beschrieben werden). Ein virtueller Thread beginnt dagegen mit nur etwa 1 KB, was ihn um Größenordnungen leichter macht.

Diese Einsparung ermöglicht es, sehr viele Threads gleichzeitig zu betreiben, ohne dass der Speicherbedarf explodiert – ein entscheidender Vorteil für serverseitige Anwendungen mit hoher Parallelität.

Effizientes Blocking – ohne OS-Thread-Blockaden

Wenn ein Plattform-Thread blockiert (z. B. bei einer Datenbank- oder Netzwerkoperation), bleibt er an einen nativen Thread gebunden – die CPU kann in dieser Zeit nichts anderes damit anfangen. Virtuelle Threads hingegen können blockieren, ohne den zugrunde liegenden Carrier Thread zu blockieren. Die JVM verschiebt dabei den Stack des virtuellen Threads in den Heap (Stack Parking) und gibt den Carrier Thread sofort frei, damit andere Aufgaben ausgeführt werden können.

Dieses Blocking ist zwar nicht völlig kostenlos – da das Verschieben des Stacks Rechenzeit kostet – aber im Vergleich zu echten OS Threads dramatisch günstiger.

Schnellere Context Switches

Ein weiterer Performance-Vorteil liegt in den sogenannten Context Switches – dem Wechsel zwischen zwei Threads. Bei Plattform-Threads muss dieser Wechsel im Kernel Space des Betriebssystems erfolgen, was teuer ist. Virtuelle Threads führen Context Switches dagegen im User Space aus, direkt in der JVM. Dort wurden zahlreiche Optimierungen implementiert, sodass diese Wechsel wesentlich schneller und ressourcenschonender ablaufen.

Einfach in bestehende Anwendungen integrierbar

Trotz der tiefgreifenden technischen Änderungen bleiben virtuelle Threads vollständig kompatibel mit bestehenden Java APIs. Die bekannten Klassen Thread und ExecutorService wurden nur minimal erweitert, sodass der Umstieg intuitiv und reibungslos gelingt.

Virtuelle Threads können mit den gleichen Werkzeugen wie bisher debuggt, beobachtet und profiliert werden. Das heißt: Entwickler profitieren von den neuen Möglichkeiten, ohne ihre gewohnten Entwicklungs- und Test-Workflows anpassen zu müssen.

Virtuelle Threads kombinieren die Vertrautheit des klassischen Thread-Modells mit der Effizienz moderner Nebenläufigkeitskonzepte.

Sie starten schneller, benötigen weniger Speicher, blockieren Ressourcen nicht unnötig und lassen sich leicht in bestehende Java-Anwendungen integrieren – ein echter Meilenstein in der Evolution der Java-Plattform.

Nachteile und Fallstricke

So beeindruckend die Vorteile virtueller Threads auch sind – sie sind kein Allheilmittel. Wer sie einsetzt, sollte ihre Grenzen und potenziellen Stolperfallen kennen, um unerwartete Performance-Einbrüche oder falsche Erwartungen zu vermeiden.

CPU-intensive Aufgaben: kein magisches Multithreading

Virtuelle Threads sind keine schnelleren Threads. Sie können nicht mehr CPU-Anweisungen pro Zeiteinheit ausführen als ein herkömmlicher Plattform-Thread.

Beim Start legt die JVM standardmäßig so viele Carrier Threads an, wie logische CPU-Kerne vorhanden sind. Jeder dieser Carrier Threads arbeitet sequenziell Aufgaben ab. Solange ein virtueller Thread aktiv CPU-Zeit beansprucht (z. B. bei komplexen Berechnungen), bleibt er auf seinem Carrier Thread "montiert" und blockiert diesen.

Das bedeutet: Wenn 20 Carrier Threads existieren und 20 virtuelle Threads CPU-intensiv arbeiten, wird kein weiterer virtueller Thread ausgeführt, bis einer der laufenden Threads seine Arbeit beendet oder blockiert.

Virtuelle Threads sind also nicht geeignet für reine Rechenlasten – sie entfalten ihre Stärke erst, wenn viele Aufgaben auf I/O warten (z. B. Datenbank- oder Netzwerkzugriffe).

Overhead bei nicht blockierenden Aufgaben

Da virtuelle Threads zusätzliche Verwaltungslogik für das Mounting und Unmounting benötigen, kann eine nicht blockierende Aufgabe auf einem virtuellen Thread sogar langsamer laufen als auf einem klassischen Plattform-Thread, der über einen ExecutorService verwaltet wird.

Für reine CPU Tasks ist es daher oft effizienter, weiterhin herkömmliche Threads oder Pools zu verwenden.

Nicht unterstützte blockierende Operationen

Das JDK wurde weitgehend angepasst, um virtuelle Threads zu unterstützen, insbesondere bei klassischen Blockierungen wie I/O- oder Sleep-Operationen. Es gibt jedoch noch einige wenige APIs oder Bibliotheken, die nicht kompatibel sind. Wenn solche Methoden verwendet werden, kann die JVM den virtuellen Thread nicht vom Carrier Thread trennen, wodurch der Vorteil der hohen Parallelität verloren geht.

Thread Pinning

Ein weiteres Risiko ist das sogenannte Thread Pinning: Wie zuvor beschrieben wird ein virtueller Thread vorübergehend fest an einen Carrier Thread gebunden, obwohl er eigentlich freigegeben werden könnte und blockiert diesen damit. 

Ein gepinnter Thread verhindert, dass der Carrier Thread andere virtuelle Threads ausführen kann – was zu verringerter Parallelität und höherer Latenz führt.

Lebenszyklus: join() nicht vergessen

Ein kleiner, aber häufiger praktischer Stolperstein: Virtuelle Threads beenden sich automatisch, sobald der aufrufende Thread (z. B. main) endet. Das heißt, wenn der Haupt-Thread nicht auf die virtuellen Threads wartet, werden sie möglicherweise vorzeitig beendet, bevor ihre Arbeit abgeschlossen ist. 

Abschluss

Virtuelle Threads stellen eine der bedeutendsten Weiterentwicklungen der Java-Plattform dar. Sie bieten eine leistungsfähige und ressourcenschonende Alternative zu klassischen Plattform-Threads und ermöglichen es, eine enorme Anzahl paralleler Aufgaben effizient zu verarbeiten – insbesondere in I/O-lastigen Anwendungen.

Dennoch sind virtuelle Threads kein Allheilmittel. Ihre optimale Nutzung setzt ein gutes Verständnis ihrer Funktionsweise voraus. Vor allem bei CPU-intensiven Tasks oder in Szenarien mit Pinned Threads kann der erwartete Performancegewinn ausbleiben.

Wer virtuelle Threads einsetzt, sollte daher bewusst prüfen und testen, ob sie für die jeweilige Anwendung die richtige Wahl sind – und sicherstellen, dass sie nicht versehentlich ineffizient verwendet werden. Richtig eingesetzt, sind sie jedoch ein großer Schritt in Richtung moderner, skalierbarer Java-Anwendungen.

Autor
Das könnte Sie auch interessieren
Kommentare (0)

Neuen Kommentar schreiben