1. Skalierbarkeit
Warum skalieren klassische Threads schlecht?
Jeder klassische Thread (Thread) ist eine Betriebssystem-Entität mit eigenem Stack (typisch 1–2 MB) und Zustandsstruktur. Der Versuch, z. B. 10 000 klassische Threads zu erzeugen, führt häufig zu einem OutOfMemoryError. Daher nutzen traditionelle Server begrenzte Thread-Pools.
Virtuelle Threads: die Magie der Skalierbarkeit
Virtuelle Threads (Java 21+) sind „leichte“ Threads, die von der JVM verwaltet werden. Ihr Stack liegt im Heap und kann dynamisch wachsen oder schrumpfen. Wenn ein Thread bei I/O blockiert, „parkt“ die JVM ihn und führt andere Aufgaben weiter aus.
In der JVM läuft ein kleiner Pool von „Carrier-Threads“ (carrier threads) – Betriebssystem-Threads, auf denen virtuelle Threads nacheinander ausgeführt werden. Das ermöglicht 100 000+ Aufgaben ohne massiven Speicherbedarf. Die JVM plant selbst, welche virtuellen Threads wann laufen.
Demonstration: 100_000 virtuelle Threads gegen 1_000 Plattform-Threads
Beispiel: Erzeugung von 1000 klassischen Threads
// Versuch, 1000 Plattform-Threads zu erstellen
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
Thread t = new Thread(() -> {
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
threads.add(t);
t.start();
}
System.out.println("Erstellte Threads: " + threads.size());
Ergebnis: Auf den meisten Systemen lassen sich 1 000–2 000 Threads erzeugen; bei größeren Werten treten Speicherprobleme und Verlangsamungen auf.
Beispiel: Erzeugung von 100 000 virtuellen Threads
// Wir erzeugen 100_000 virtuelle Threads
List<Thread> vThreads = new ArrayList<>();
for (int i = 0; i < 100_000; i++) {
Thread t = Thread.ofVirtual().start(() -> {
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
vThreads.add(t);
}
System.out.println("Erstellte virtuelle Threads: " + vThreads.size());
Ergebnis: Das Programm erstellt problemlos 100 000 virtuelle Threads ohne Abstürze oder nennenswerte Verzögerungen. Der Speicherbedarf ist um ein Vielfaches geringer.
Visueller Vergleich
| Thread-Typ | Maximale Thread-Anzahl (ca.) | Speichernutzung | Startzeit |
|---|---|---|---|
| Plattform-Threads (Thread) | 1 000 – 10 000 | Hoch | Langsam |
| Virtuelle | 100 000 – 1 000 000+ | Niedrig | Sofort |
Fakt: Virtuelle Threads erlauben es, Code nach dem Muster „ein Thread pro Aufgabe“ zu schreiben – ohne komplexe Pools und ohne das Risiko, das System zu überlasten.
2. Performance: wo virtuelle Threads ihre Stärken ausspielen
Aufgaben, die durch I/O limitiert sind (I/O-bound)
Virtuelle Threads eignen sich hervorragend für Netzwerkaufrufe, Datei-I/O und Arbeit mit der DB. Wenn eine Operation blockiert, gibt der virtuelle Thread seinen „Carrier“ frei und die JVM führt andere Aufgaben aus. Das erhöht den Durchsatz bei vielen gleichzeitigen Wartezeiten.
Beispiel: Simulation von 10 000 gleichzeitigen HTTP-Anfragen
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
HttpClient client = HttpClient.newHttpClient();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10_000; i++) {
Thread t = Thread.ofVirtual().start(() -> {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com"))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Antwort: " + response.statusCode());
} catch (Exception e) {
System.out.println("Fehler: " + e.getMessage());
}
});
threads.add(t);
}
// Auf alle Threads warten
for (Thread t : threads) {
t.join();
}
Ergebnis: Alle 10 000 Anfragen laufen parallel; das Programm stürzt nicht ab, und der Code bleibt einfach.
CPU-bound: Virtuelle Threads beschleunigen Rechenarbeit nicht
Wenn eine Aufgabe die CPU auslastet, bringen virtuelle Threads keine zusätzliche Geschwindigkeit: Die Zahl der Kerne ist fest. Hier sind fixe Pools in der Größe der Kernanzahl sinnvoll, um unnötige Konkurrenz zu vermeiden.
// Jede Aufgabe berechnet die Summe eines großen Bereichs
Runnable cpuTask = () -> {
long sum = 0;
for (int i = 0; i < 100_000_000; i++) {
sum += i;
}
System.out.println("Summe: " + sum);
};
// Wir starten 1000 virtuelle Threads mit Berechnungen
for (int i = 0; i < 1000; i++) {
Thread.ofVirtual().start(cpuTask);
}
Ergebnis: Die Threads konkurrieren um CPU, aber es gibt keinen Geschwindigkeitsgewinn – das ist kein Einsatzgebiet für Virtual Threads.
3. Einschränkungen und Besonderheiten
Synchronisation und Fallstricke bei virtuellen Threads
- Vorsicht mit nativen Sperren. Die Verwendung von synchronized kann einen virtuellen Thread an den Carrier binden und so den Vorteil schmälern. Bevorzugen Sie ReentrantLock, Semaphore und andere Primitive aus java.util.concurrent, die für virtuelle Threads optimiert sind.
- Legacy-Bibliotheken. Manche JDBC-Treiber und native Bibliotheken sind noch nicht für Virtual Threads optimiert. Testen Sie blockierende Operationen gründlich.
Nicht für langlebige Aufgaben
Virtual Threads sind ideal für „kurze“ Arbeitseinheiten: eine Anfrage verarbeiten, eine Operation ausführen und beenden. Millionen ewig lebender Tasks (z. B. endlose Berechnungen) bringen keinen Nutzen – dafür verwenden Sie Plattform-Threads.
4. Best Practices: wo virtuelle Threads sinnvoll sind
- I/O-bound-Aufgaben: Netzwerkaufrufe, Dateien, DB – überall, wo der Thread oft wartet.
- Webserver: Bearbeiten Sie jede HTTP-Anfrage in einem eigenen virtuellen Thread.
- Integrationstests: Simulieren Sie schnell Tausende von Clients.
- Asynchrone Verarbeitung: Schreiben Sie gewohnten „blockierenden“ Code – die JVM übernimmt intelligentes Scheduling.
Nicht sinnvoll:
- Für Aufgaben, die die CPU dauerhaft auslasten.
- Wenn Kompatibilität mit Low-Level-Bibliotheken kritisch ist (noch nicht alle sind angepasst).
Unter der Haube ist der Executor praktisch: Executors.newVirtualThreadPerTaskExecutor() – „ein virtueller Thread pro Aufgabe“, ohne festen Pool.
5. Monitoring und Messen: virtuelle Threads in Aktion sehen
JVisualVM und Flight Recorder
JVisualVM zeigt aktive Threads, deren Zustände und Speicher; seit Java 21 werden virtuelle Threads separat angezeigt. Java Flight Recorder (JFR) schreibt eine detaillierte „Blackbox“ der Ausführung, inklusive Statistik zu Virtual Threads – ideal zum Finden von Engpässen.
So sehen Sie die Anzahl der Threads im Code
Ein einfacher Weg, die Anzahl der Threads in der JVM zu sehen:
System.out.println("Gesamtzahl der Threads: " + Thread.activeCount());
Zählen, wie viele davon virtuell sind:
long vCount = Thread.getAllStackTraces().keySet().stream()
.filter(Thread::isVirtual)
.count();
System.out.println("Virtuelle Threads: " + vCount);
6. Typische Fehler beim Einsatz virtueller Threads
Fehler Nr. 1: Virtuelle Threads für rechenintensive Aufgaben einsetzen. Millionen virtueller Threads mit CPU-bound-Aufgaben beschleunigen den Prozessor nicht. Virtual Threads sind kein „Turbo“ für Berechnungen.
Fehler Nr. 2: Blindes Übernehmen alter Muster. Erstellen Sie keine festen Pools virtueller Threads. Verwenden Sie Executors.newVirtualThreadPerTaskExecutor() und lassen Sie die JVM automatisch skalieren.
Fehler Nr. 3: Verwendung nicht unterstützter Bibliotheken. Native Sperren und nicht für Loom angepasste Bibliotheken können Hänger und Einbrüche verursachen. Prüfen Sie die Kompatibilität im Voraus.
Fehler Nr. 4: Vorzeitige Optimierung. Wenn Sie nur wenige Threads und normale Nebenläufigkeit haben, migrieren Sie nicht übereilt alles auf Virtual Threads. Das Werkzeug ist dort stark, wo massives I/O und Warten vorliegt.
Fehler Nr. 5: Monitoring ignorieren. Millionen Tasks zu erzeugen ist leicht, aber ohne Monitoring und Exception-Handling bekommen Sie „einen hübschen Benchmark“ statt eines zuverlässigen Systems. Verwenden Sie JVisualVM und JFR.
GO TO FULL VERSION