1. Wie man virtuelle Threads in der Praxis erstellt
Zeit, von der Theorie zur Praxis zu wechseln! Sie wissen bereits, wie man klassisch einen Thread erzeugt:
Thread t = new Thread(() -> System.out.println("Hello from thread!"));
t.start();
Oder etwas kürzer:
new Thread(() -> System.out.println("Hi!")).start();
Mit Java 21 gibt es nun eine neue Möglichkeit:
Thread.startVirtualThread(() -> System.out.println("Hello from virtual thread!"));
oder expliziter:
Thread t = Thread.ofVirtual().start(() -> System.out.println("Hello from virtual thread!"));
Worin besteht der Unterschied?
- Thread.ofVirtual().start(...) erzeugt einen virtuellen Thread (Virtual Thread), der von der JVM und nicht vom Betriebssystem verwaltet wird.
- Thread.ofPlatform().start(...) (oder new Thread(...)) — ein klassischer Thread wie bisher.
Warum ist das wichtig?
Virtuelle Threads lassen sich zu Zehntausenden erzeugen, ohne einen OutOfMemoryError befürchten zu müssen. Wenn Sie sich plötzlich entscheiden, eine Million Anfragen zu verarbeiten – Java wird sagen: „Kein Problem, nur zu!“
2. Syntax zum Erstellen eines virtuellen Threads
Grundbeispiel:
public class VirtualThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.ofVirtual().start(() -> {
System.out.println("Hallo aus dem virtuellen Thread! Thread: " + Thread.currentThread());
});
// Auf das Ende des Threads warten (damit main nicht vorher beendet wird)
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Was passiert hier?
- Wir erzeugen einen virtuellen Thread über Thread.ofVirtual().start(...).
- Im Thread passiert etwas Einfaches: eine Meldung wird ausgegeben.
- Zum Schluss rufen wir thread.join() auf, damit der Haupt-Thread auf die Beendigung des virtuellen Threads wartet (sonst könnte das Programm enden, bevor der Thread etwas ausgibt).
Beachten Sie:
Virtuelle Threads sehen aus und verhalten sich fast wie normale – aber innen drin wirkt die Magie der JVM!
3. Massenerzeugung virtueller Threads: die Kraft von Loom in der Praxis
Nun probieren wir etwas, das mit normalen Threads riskant (oder schlicht unmöglich) wäre: Wir erzeugen 10_000 virtuelle Threads, von denen jeder seine Nummer ausgibt.
public class VirtualThreadMassive {
public static void main(String[] args) throws InterruptedException {
int N = 10_000;
Thread[] threads = new Thread[N];
for (int i = 0; i < N; i++) {
int threadNum = i;
threads[i] = Thread.ofVirtual().start(() -> {
System.out.println("Virtueller Thread #" + threadNum + " läuft!");
});
}
// Auf das Ende aller Threads warten
for (Thread t : threads) {
t.join();
}
System.out.println("Alle virtuellen Threads sind beendet!");
}
}
- Für normale Threads (new Thread(...)) bringt solcher Code Ihr Programm fast sicher mit einem OutOfMemoryError zum Absturz.
- Für virtuelle Threads ist das der Normalfall! Die JVM verarbeitet mühelos Tausende bis Zehntausende Threads.
Übrigens: Wenn Ihnen 10_000 viel erscheinen, probieren Sie 100_000 oder sogar 1_000_000. Auf einem modernen Rechner kommt die JVM damit zurecht, sofern Ihre Threads einfache Arbeit verrichten oder auf I/O warten.
4. Runnable und Lambda-Ausdrücke: Code an den virtuellen Thread übergeben
Virtuelle Threads nehmen Aufgaben wie gewöhnliche Threads entgegen: über das Interface Runnable. Das bedeutet, Sie können Lambda-Ausdrücke, Method-References und beliebige Objekte, die Runnable implementieren, übergeben.
Beispiel mit Lambda:
Thread.ofVirtual().start(() -> System.out.println("Lambda im virtuellen Thread!"));
Beispiel mit Methodenreferenz:
public class TaskRunner {
public static void main(String[] args) {
Thread.ofVirtual().start(TaskRunner::doWork);
}
static void doWork() {
System.out.println("Wir arbeiten im virtuellen Thread: " + Thread.currentThread());
}
}
Beispiel mit anonymer Klasse:
Thread.ofVirtual().start(new Runnable() {
@Override
public void run() {
System.out.println("Anonyme Klasse im virtuellen Thread!");
}
});
Fazit:
Alles, was mit normalen Threads funktionierte, funktioniert auch mit virtuellen – nur jetzt „leicht und schnell“.
5. Vergleich mit dem ExecutorService: alter und neuer Ansatz
Klassischer ExecutorService
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
int taskNum = i;
executor.submit(() -> {
System.out.println("Aufgabe #" + taskNum + " wird ausgeführt");
});
}
executor.shutdown();
Problem:
Wenn es zu viele Aufgaben und zu wenige Threads gibt, warten die Aufgaben in der Warteschlange. Gibt es zu viele Threads, „verschluckt“ sich das Programm an Ressourcenknappheit.
Neuer Ansatz: Executor auf virtuellen Threads
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 0; i < 100_000; i++) {
int taskNum = i;
executor.submit(() -> {
System.out.println("Virtuelle Aufgabe #" + taskNum);
});
}
executor.shutdown();
Was passiert hier?
- Für jede Aufgabe wird ein eigener virtueller Thread erzeugt.
- Die JVM übernimmt das Scheduling, ohne das System zu überlasten.
- Es ist nicht nötig, die Poolgröße zu begrenzen – virtuelle Threads sind „fast kostenlos“.
Wann sollte man einen Executor auf virtuellen Threads einsetzen?
- Wenn Sie einen großen Aufgabenstrom haben und sich nicht um die Poolgröße kümmern möchten.
- Wenn die Aufgaben unabhängig sind und parallel ausgeführt werden können.
- Wenn Sie virtuelle Threads in eine bestehende Architektur integrieren möchten, die bereits einen ExecutorService verwendet (z. B. in einem Webserver, einem Task-Handler usw.).
6. Praktische Tipps: Wann was verwenden
Wann direkt Thread.ofVirtual().start() verwenden?
- Wenn Sie für eine einzelne Aufgabe einen separaten Thread benötigen (z. B. für Tests, Demos oder ein einfaches Experiment).
- Wenn es nur wenige Threads gibt und Sie sie manuell steuern möchten.
Wann Executors.newVirtualThreadPerTaskExecutor() verwenden?
- Wenn Sie Aufgaben massenhaft starten müssen (z. B. bei der Verarbeitung vieler Anfragen, Dateien, Netzwerkverbindungen).
- Wenn die Aufgaben unabhängig sind und keine Koordination untereinander erfordern.
- Wenn Sie virtuelle Threads in eine bestehende Architektur integrieren möchten, die bereits einen ExecutorService verwendet (z. B. in einem Webserver, einem Task-Handler usw.).
Tipp:
Wenn Sie unsicher sind – starten Sie mit einem Executor auf virtuellen Threads. Das ist der universellste und modernste Ansatz.
7. Ausnahmebehandlung in virtuellen Threads
Virtuelle Threads sind aus Sicht von try-catch ganz normale Threads. Wenn innerhalb Ihres Runnable eine Ausnahme auftritt, bringt sie nicht die gesamte JVM zum Absturz, sondern beendet lediglich den betreffenden Thread mit Fehler.
Beispiel:
Thread t = Thread.ofVirtual().start(() -> {
throw new RuntimeException("Etwas ist schiefgelaufen!");
});
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Der Haupt-Thread hat weitergearbeitet.");
Im ExecutorService:
Wenn Sie eine Aufgabe über submit senden, können Sie das Ergebnis über Future erhalten, und die Ausnahme wird beim Aufruf von get() ausgelöst:
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Future<?> f = executor.submit(() -> {
throw new RuntimeException("Fehler in der virtuellen Aufgabe");
});
try {
f.get();
} catch (ExecutionException e) {
System.out.println("Fehler aus dem virtuellen Thread abgefangen: " + e.getCause());
}
executor.shutdown();
8. Typische Fehler beim Erzeugen virtueller Threads
Fehler Nr. 1: Virtuelle und Plattform-Threads verwechseln. Wenn Sie Threads über new Thread(...) oder Thread.ofPlatform() erzeugen, sind das keine virtuellen Threads. Nur Thread.ofVirtual().start(...) oder Methoden aus Executors liefern echte Virtual Threads.
Fehler Nr. 2: Beschleunigung für rechenintensive Aufgaben erwarten. Virtuelle Threads beschleunigen stark CPU-gebundene Aufgaben nicht. Wenn Sie eine Million Threads haben, die jeweils Pi bis zur millionsten Stelle berechnen – die JVM kann die Berechnung nicht „beschleunigen“, sie wird lediglich zwischen Threads umschalten.
Fehler Nr. 3: Pro Thread eigene Ressourcen (z. B. Datenbank) halten. Wenn Sie eine Million virtuelle Threads erzeugen, aber jeder eine eigene DB-Verbindung braucht, hält die Datenbank das nicht aus. Virtuelle Threads eignen sich für Aufgaben, bei denen der Großteil der Zeit aus Warten (I/O) besteht, nicht für Arbeit mit knappen externen Ressourcen.
Fehler Nr. 4: Nicht auf das Ende der Threads warten, obwohl es wichtig ist. Wenn der Haupt-Thread endet, bevor die virtuellen Threads fertig sind, kann das Programm ohne Ergebnisse beenden. Verwenden Sie join() oder einen ExecutorService mit shutdown() und awaitTermination().
Fehler Nr. 5: Veraltete, nicht mit virtuellen Threads kompatible Bibliotheken einsetzen. Manche Fremdbibliotheken blockieren Threads auf Betriebssystemebene oder verwenden native Synchronisation – das mindert die Effizienz virtueller Threads. Prüfen Sie die Kompatibilität stets.
GO TO FULL VERSION