1. Klassische Threads: wie es funktioniert und wo es wehtut
Erinnern wir uns zunächst daran, wie gewöhnliche Threads in Java funktionieren (auch Plattform- oder native Threads genannt). Genau die, die via new Thread(...) erstellt werden.
Wenn Sie new Thread(() -> { ... }).start(); aufrufen, startet die JVM nicht einfach ein Stück Code. Sie bittet das Betriebssystem, einen echten Ausführungs-Thread zu erzeugen. Das OS weist dafür einen eigenen Stack zu (typischerweise einige Megabyte) und reserviert weitere administrative Ressourcen.
Ein solcher Thread lebt, solange seine Aufgabe läuft, und belegt die ganze Zeit einen Eintrag in der Thread-Tabelle des Betriebssystems. Je mehr solcher Threads es gibt, desto mehr Speicher geht für ihre Stacks drauf und desto höher ist die Last auf dem OS. Deshalb kann eine Anwendung bei sehr vielen gleichzeitig laufenden Threads „ins Stottern geraten“ – das System verbringt zu viel Zeit mit dem Umschalten.
Beispiel: klassischer Thread
Thread thread = new Thread(() -> {
System.out.println("Hallo aus dem Thread!");
});
thread.start();
Sieht einfach aus, oder? Aber erstellen Sie nicht nur ein oder zwei, sondern sagen wir zehntausend solcher Threads – und Ihr Programm gerät schnell in Atemnot. Entweder geht der Speicher aus oder das System meldet, dass das Thread-Limit erreicht ist. Das ist kein Fehler von Java, sondern eine Folge der Architektur: Threads sind schwergewichtig und teuer.
Warum ist das so? Jeder Thread erhält einen eigenen Stack (üblich sind 1–2 Megabyte), plus einen ganzen Satz an Verwaltungsstrukturen vom Betriebssystem. Und das OS ist zudem nicht begeistert, wenn man ihm zehntausende Threads unterjubelt – es gibt harte Grenzen.
Und selbst wenn der Speicher nicht ausgeht, gibt es ein anderes Problem – Context Switches. Sind zu viele Threads aktiv, springt das System ständig zwischen ihnen hin und her und speichert bzw. restauriert ihren Zustand. Das kostet Zeit und Performance; von „Massen-Parallelisierung“ profitiert man dann nicht.
Das Problem „ein Thread – eine Anfrage“
In älteren Serveranwendungen (z. B. auf Tomcat oder Jetty) wurde häufig das Modell „thread‑per‑request“ verwendet: Für jede eingehende Benutzeranfrage wurde ein eigener Thread bereitgestellt. Das ist bequem, aber wenn Sie 10_000 Nutzer haben, brauchen Sie 10_000 Threads! Der Server gerät unter Druck, und es beginnt ein Rennen um Speicher statt um Verarbeitungsgeschwindigkeit.
Fazit:
Klassische Threads eignen sich für eine geringe Anzahl paralleler Aufgaben, skalieren aber nicht auf Zehn- oder Hunderttausende.
2. Was sind virtuelle Threads (Virtual Threads)?
Hier kommt der Held der heutigen Vorlesung ins Spiel – virtuelle Threads. Das ist nicht einfach „nur ein weiterer Thread“, sondern ein völlig anderer Architekturansatz.
Virtuelle Threads sind Threads, die nicht vom Betriebssystem, sondern von der JVM selbst verwaltet werden. Sie sind komplett in Java implementiert und können in riesiger Zahl (Zehn- und Hunderttausende) erzeugt werden – ohne Speicheraufblähung und ohne Bremsen.
Kurz:
- Platform Thread (Plattform-Thread): ein gewöhnlicher Thread, der direkt einem OS-Thread entspricht.
- Virtual Thread (virtueller Thread): ein leichter Thread, der von der JVM und nicht vom OS verwaltet wird.
Wie ist das aufgebaut?
Virtuelle Threads sind „leichte“ Threads, die nicht im Betriebssystem, sondern innerhalb der JVM leben. Sie laufen auf einem kleinen Pool echter Threads, den sogenannten platform threads. Man kann sich das so vorstellen, dass die JVM sie wie ein Dirigent verwaltet: Es gibt eine begrenzte Zahl von Musikern (echte Threads), aber die Partituren (virtuelle Threads) werden geschickt zwischen ihnen verteilt.
Architektur-Skizze:
+-------------------+ +-------------------+
| Virtual Thread 1 |---\ | Platform Thread |
| Virtual Thread 2 |---->====> | (Carrier Thread) |
| Virtual Thread 3 |---/ +-------------------+
... (Betriebssystem)
Carrier Thread – das ist ein gewöhnlicher OS-Thread, auf dem die JVM viele virtuelle Threads ausführt. Blockiert ein virtueller Thread – etwa weil er auf Daten von Platte oder aus dem Netzwerk wartet –, „friert“ die JVM ihn einfach ein und gibt den carrier thread für andere Aufgaben frei.
Warum ist das revolutionär?
Weil man nun gewohnten, linearen Code schreiben kann – ohne endlose Callbacks, CompletableFuture und „höllische“ thenApply-Ketten – und trotzdem auf Tausende gleichzeitige Operationen skalieren kann.
Virtuelle Threads belegen nur Dutzende Kilobyte (statt Megabyte bei klassischen) und werden nahezu sofort erzeugt. Daher kann man sie zu Tausenden starten und wieder beenden, ohne befürchten zu müssen, dass das Betriebssystem unter ihrer Last zusammenbricht. Das macht nebenläufiges Programmieren in Java endlich leicht und natürlich.
3. Vorteile virtueller Threads
Skalierbarkeit
Mit virtuellen Threads können Sie sich den Luxus leisten, Zehn- und Hunderttausende paralleler Aufgaben zu starten. Zum Beispiel jede Netzwerkanfrage in einem eigenen Thread verarbeiten – ohne Sorge, dass der Server „platzt“.
Demonstration: 100.000 virtuelle Threads
for (int i = 0; i < 100_000; i++) {
Thread.ofVirtual().start(() -> {
// Hier kann beliebige Logik stehen
try {
Thread.sleep(1000); // Arbeit simulieren
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
System.out.println("Alle Threads sind gestartet!");
Dieser Code läuft problemlos auf einem gewöhnlichen Laptop!
Versuchen Sie dasselbe mit klassischen Threads – und Sie sehen entweder OutOfMemoryError oder Ihr Rechner wird praktisch unbenutzbar.
Einfachheit der Programmierung
Virtuelle Threads erlauben es, gewohnten „blockierenden“ Code zu schreiben, ohne ihn in „Spaghetti“ aus asynchronen Aufrufen zu verwandeln. Sie können z. B. Thread.sleep, InputStream.read, Socket.accept ganz normal verwenden – die JVM sorgt dafür, dass dabei nicht der gesamte carrier thread blockiert.
Bessere Lesbarkeit und Wartbarkeit
Anstelle komplexer Schemata mit Callbacks und CompletableFuture schreiben Sie linearen, verständlichen Code. Das reduziert Bugs und erleichtert die Wartung.
Kein Rad neu erfinden
Früher musste man, um Tausende Anfragen parallel zu verarbeiten, asynchrone Frameworks, reaktive Bibliotheken (Netty, Vert.x, Project Reactor) nutzen, die einen besonderen Programmierstil verlangen. Jetzt kommt man oft ohne sie aus – und erhält dennoch Skalierbarkeit.
4. Architektur: wie virtuelle Threads „unter der Haube“ funktionieren
Mapping auf Carrier Threads
Die JVM erstellt einen kleinen Pool echter Threads (Carrier Threads) – in der Regel so viele, wie CPU‑Kerne vorhanden sind. Alle virtuellen Threads „fahren“ auf diesen Carrier Threads wie Fahrgäste in Bussen.
- Wenn ein virtueller Thread blockiert (z. B. auf eine Antwort aus dem Netzwerk wartet), „entlädt“ die JVM ihn vom Carrier Thread und stellt ihn in die Warteschlange.
- Sobald der Thread weiterlaufen kann, setzt die JVM ihn wieder auf einen freien Carrier Thread.
Analogie:
Stellen Sie sich vor, Sie haben 4 Taxis (Carrier Threads) und bedienen 10.000 Kunden (Virtual Threads). Sobald ein Kunde angekommen ist und aussteigt, nimmt das Taxi sofort den nächsten mit. Niemand steht herum, und die Taxis brechen nicht unter der Last der Fahrgäste zusammen.
Scheduling und Umschalten
Die JVM entscheidet selbst, welcher virtuelle Thread gerade ausgeführt wird. Blockiert ein Thread auf I/O, behindert er die anderen nicht.
5. Einschränkungen und Besonderheiten virtueller Threads
Nicht alles, was virtuell ist, ist Gold
Nicht für lange Berechnungen: Wenn eine Aufgabe die CPU dauerhaft auslastet (heavy CPU‑bound), bringt ein virtueller Thread keinen Performancegewinn. Denn die Carrier Threads sind dennoch auf die Anzahl der Kerne begrenzt.
Manche Sperren sind ineffizient: Alte Synchronisationsmechanismen (z. B. Synchronisation via synchronized auf Objekten mit nativen Mutexen) können verhindern, dass die JVM einen virtuellen Thread „einfriert“. In solchen Fällen wartet der Carrier Thread mit, was die Skalierbarkeit reduziert.
Nicht alle Bibliotheken sind kompatibel mit virtuellen Threads: Wenn eine Bibliothek native Aufrufe macht oder spezielle Sperren verwendet, können sich virtuelle Threads anders verhalten als erwartet.
Beispiel: Wann man Virtual Threads nicht einsetzen sollte
Wenn Sie eine Aufgabe haben, die in einer Endlosschleife Zahlen rechnet, bringen virtuelle Threads keinen Vorteil. Man stößt trotzdem an die Kernanzahl.
Thread.ofVirtual().start(() -> {
while (true) {
// Zählen bis unendlich
}
});
Ergebnis:
Ein Carrier Thread wird von diesem virtuellen Thread voll belegt, und andere Aufgaben warten auf ihre Chance.
6. Vergleich: Platform Thread vs. Virtual Thread
| Merkmal | Platform Thread (klassisch) | Virtual Thread (virtuell) |
|---|---|---|
| Gesteuert von | dem Betriebssystem | JVM |
| Speicher pro Thread | Megabyte | Dutzende Kilobyte |
| Anzahl der Threads | In der Regel < 10_000 | Tausende, Hunderttausende |
| Kosten der Erstellung | Teuer | Günstig |
| Skalierbarkeit | Begrenzt | Kaum begrenzt |
| Geeignet für | Lange, CPU‑bound Aufgaben | Kurze, I/O‑bound Aufgaben |
| Thread-Umschaltung | OS | JVM |
| Kompatibilität | 100% | Fast immer, aber es gibt Besonderheiten |
7. Beispiel: Wie ein Server vor und nach Virtual Threads aussähe
Vorher (Platform Threads)
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket client = serverSocket.accept();
new Thread(() -> handleClient(client)).start();
}
Problem:
Ab etwa 5.000 Verbindungen gerät der Server ins Stocken.
Nachher (Virtual Threads, Java 21+)
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket client = serverSocket.accept();
Thread.ofVirtual().start(() -> handleClient(client));
}
Magie:
Jetzt lassen sich Zehntausende Verbindungen bearbeiten – und man muss nicht an Thread-Limits denken!
9. Typische Fehler beim Umstieg auf virtuelle Threads
Fehler Nr. 1: Einen Geschwindigkeitsschub für Rechenlast erwarten. Virtuelle Threads beschleunigen Aufgaben nicht, die die CPU vollständig auslasten. Für solche Aufgaben bleibt man durch die Kernanzahl begrenzt.
Fehler Nr. 2: Alte blockierende Synchronisationen verwenden. Wenn Sie alte Sperren verwenden (z. B. synchronized auf Objekten, die nativ „verriegelt“ werden können), werden virtuelle Threads möglicherweise nicht vom Carrier Thread entladen – die Vorteile gehen verloren.
Fehler Nr. 3: Verhalten von Drittbibliotheken nicht berücksichtigen. Manche Drittbibliotheken sind nicht auf virtuelle Threads vorbereitet (z. B. wenn sie JNI nutzen oder native Sperren einsetzen).
Fehler Nr. 4: Einen magischen Performance‑Sprung erwarten. Virtuelle Threads sind keine Allheilmittel. Sie beschleunigen nicht alles, sondern machen Parallelismus vor allem für I/O‑bound Aufgaben günstig und bequem.
GO TO FULL VERSION