Virtual Threads (Project Loom)
Was sind Virtual Threads?
Virtual Threads sind leichtgewichtige Threads, die von der JVM verwaltet werden, nicht vom Betriebssystem.
Hauptvorteile:
- Extrem leichtgewichtig – Millionen können gleichzeitig existieren
- JVM-verwaltet – kein 1:1-Mapping zu OS-Threads
- Carrier Threads – Virtual Threads werden auf Platform Threads ausgeführt
- Automatisches Unmounting – bei Blockierung wird der Virtual Thread vom Carrier Thread entfernt
Platform Threads vs Virtual Threads
// Platform Thread (traditionell)
Thread platformThread = new Thread(() -> {
// Blockiert OS-Thread
Thread.sleep(1000);
});
platformThread.start();
// Virtual Thread
Thread virtualThread = Thread.ofVirtual().start(() -> {
// Blockiert nur Virtual Thread, nicht Carrier
Thread.sleep(1000);
});Platform Threads:
- 1:1-Mapping zu OS-Threads
- Jeder ~1 MB Stack-Speicher
- Begrenzt (Tausende)
- Blockieren sperrt OS-Thread
Virtual Threads:
- N:M-Mapping (viele Virtual auf wenige Platform)
- Minimaler Speicher-Overhead
- Millionen möglich
- Blockieren sperrt nur Virtual Thread
Virtual Threads erstellen
// Methode 1: Thread.ofVirtual()
Thread vt1 = Thread.ofVirtual().start(() -> {
System.out.println("Virtual Thread 1");
});
// Methode 2: Thread.startVirtualThread()
Thread vt2 = Thread.startVirtualThread(() -> {
System.out.println("Virtual Thread 2");
});
// Methode 3: Thread.Builder
Thread vt3 = Thread.ofVirtual()
.name("my-virtual-thread")
.unstarted(() -> {
System.out.println("Virtual Thread 3");
});
vt3.start();
// Methode 4: ExecutorService
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> {
System.out.println("Virtual Thread 4");
});
}Thread-Gruppen und Namen
// Thread-Gruppe für Virtual Threads
AtomicInteger counter = new AtomicInteger(0);
Thread.Builder builder = Thread.ofVirtual()
.name("worker-", counter.getAndIncrement());
Thread t1 = builder.unstarted(() -> task());
Thread t2 = builder.unstarted(() -> task());
System.out.println(t1.getName()); // worker-0
System.out.println(t2.getName()); // worker-1Carrier Threads
Carrier Threads sind Platform Threads, die Virtual Threads ausführen.
// ForkJoinPool wird standardmäßig als Carrier-Pool verwendet
ForkJoinPool carrierPool = ForkJoinPool.commonPool();
System.out.println("Carrier threads: " + carrierPool.getParallelism());
// Normalerweise = Anzahl der CPU-KerneWie es funktioniert:
- Virtual Thread wird erstellt und einer Aufgabe zugewiesen
- Scheduler ordnet Virtual Thread einem freien Carrier Thread zu
- Bei Blockierung (I/O, sleep) wird Virtual Thread unmounted
- Carrier Thread kann einen anderen Virtual Thread ausführen
- Wenn I/O abgeschlossen ist, wird Virtual Thread wieder mounted
Law of Little’s
Little’s Law hilft, Systemdurchsatz zu berechnen:
L = λ × W
L = durchschnittliche Anzahl von Anfragen im System
λ = Ankunftsrate (Anfragen pro Sekunde)
W = durchschnittliche Zeit im System (Latenz)
Beispiel:
Wenn: 1000 RPS und durchschnittliche Latenz 100ms
Dann: L = 1000 × 0.1 = 100 gleichzeitige Anfragen
Mit Platform Threads: brauchen mindestens 100 Threads
Mit Virtual Threads: können mit weniger Carrier Threads umgehen
Praktisches Beispiel: HTTP-Server
// Alte Methode mit Platform Threads
try (var executor = Executors.newFixedThreadPool(200)) {
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket client = serverSocket.accept();
executor.submit(() -> handleClient(client));
}
}
// Neue Methode mit Virtual Threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket client = serverSocket.accept();
executor.submit(() -> handleClient(client));
}
}
void handleClient(Socket client) {
try (var in = new BufferedReader(new InputStreamReader(client.getInputStream()));
var out = new PrintWriter(client.getOutputStream())) {
String request = in.readLine();
// I/O-Blockierung - Virtual Thread wird unmounted
String response = processRequest(request);
out.println(response);
}
}Rate Limiting mit Semaphore
public class RateLimitedService {
private static final int MAX_CONCURRENT = 10;
private static final Semaphore semaphore = new Semaphore(MAX_CONCURRENT);
public String processRequest(String request) throws InterruptedException {
semaphore.acquire(); // Warten, wenn Limit erreicht
try {
// Eigentliche Verarbeitung
return doWork(request);
} finally {
semaphore.release();
}
}
private String doWork(String request) {
// Simuliere I/O
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "Processed: " + request;
}
}
// Verwendung mit Virtual Threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
RateLimitedService service = new RateLimitedService();
for (int i = 0; i < 100; i++) {
int requestId = i;
executor.submit(() -> {
try {
String result = service.processRequest("Request-" + requestId);
System.out.println(result);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
}Pinning – Wenn Virtual Threads “feststecken”
Pinning tritt auf, wenn ein Virtual Thread nicht vom Carrier Thread unmounted werden kann.
Ursachen für Pinning:
- synchronized-Blöcke (vor JDK 24)
// ❌ Verursacht Pinning (vor JDK 24)
synchronized(lock) {
Thread.sleep(1000); // Carrier Thread blockiert!
}
// ✅ Lösung: ReentrantLock verwenden
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
Thread.sleep(1000); // Virtual Thread kann unmounten
} finally {
lock.unlock();
}- Native Methoden oder ausländischer Code
// Native Aufrufe können Pinning verursachen
native void someNativeCall();JDK 24+ Verbesserung
Ab JDK 24 verursacht synchronized kein Pinning mehr!
// In JDK 24+ ist dies OK
synchronized(lock) {
Thread.sleep(1000); // Kein Pinning!
}Praktisches Beispiel: Datenbankabfragen
public class DatabaseService {
private final DataSource dataSource;
private final ReentrantLock lock = new ReentrantLock();
public List<User> getUsers() {
lock.lock();
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users");
ResultSet rs = stmt.executeQuery()) {
List<User> users = new ArrayList<>();
while (rs.next()) {
users.add(mapUser(rs));
}
return users;
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
// Parallele Datenbankabfragen mit Virtual Threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
DatabaseService db = new DatabaseService();
CompletableFuture<List<User>> usersFuture =
CompletableFuture.supplyAsync(db::getUsers, executor);
CompletableFuture<List<Order>> ordersFuture =
CompletableFuture.supplyAsync(db::getOrders, executor);
CompletableFuture<List<Product>> productsFuture =
CompletableFuture.supplyAsync(db::getProducts, executor);
CompletableFuture.allOf(usersFuture, ordersFuture, productsFuture).join();
Dashboard dashboard = new Dashboard(
usersFuture.join(),
ordersFuture.join(),
productsFuture.join()
);
}Structured Concurrency (Preview Feature)
// Strukturierte Parallelität - alle Tasks werden zusammen verwaltet
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<User> userFuture = scope.fork(() -> fetchUser(userId));
Future<List<Order>> ordersFuture = scope.fork(() -> fetchOrders(userId));
Future<Address> addressFuture = scope.fork(() -> fetchAddress(userId));
scope.join(); // Warten auf alle
scope.throwIfFailed(); // Exception werfen, wenn einer fehlschlägt
return new UserProfile(
userFuture.resultNow(),
ordersFuture.resultNow(),
addressFuture.resultNow()
);
} // Automatisches Aufräumen aller ThreadsPerformance-Vergleich
public class PerformanceTest {
// Platform Threads
public void testPlatformThreads() throws InterruptedException {
long start = System.currentTimeMillis();
try (var executor = Executors.newFixedThreadPool(1000)) {
for (int i = 0; i < 100_000; i++) {
executor.submit(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
}
long duration = System.currentTimeMillis() - start;
System.out.println("Platform threads: " + duration + "ms");
}
// Virtual Threads
public void testVirtualThreads() throws InterruptedException {
long start = System.currentTimeMillis();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100_000; i++) {
executor.submit(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
}
long duration = System.currentTimeMillis() - start;
System.out.println("Virtual threads: " + duration + "ms");
}
}
// Typische Ergebnisse:
// Platform threads: ~1000-2000ms (begrenzt durch Thread-Pool)
// Virtual threads: ~100-200ms (Millionen Virtual Threads möglich)Best Practices
-
Virtual Threads für I/O-intensive Aufgaben verwenden
// ✅ Gut für Virtual Threads executor.submit(() -> readFromFile()); executor.submit(() -> callExternalAPI()); executor.submit(() -> queryDatabase()); -
Platform Threads für CPU-intensive Aufgaben
// ✅ Besser mit Platform Threads ForkJoinPool.commonPool().submit(() -> complexCalculation()); -
Pinning vermeiden (vor JDK 24)
// ❌ Schlecht synchronized(lock) { blockingOperation(); } // ✅ Gut ReentrantLock lock = new ReentrantLock(); lock.lock(); try { blockingOperation(); } finally { lock.unlock(); } -
ThreadLocal mit Vorsicht verwenden
// ⚠️ ThreadLocal mit Millionen Virtual Threads = Speicherproblem ThreadLocal<Heavy Object> threadLocal = new ThreadLocal<>(); // ✅ Besser: ScopedValue verwenden (Preview) ScopedValue<HeavyObject> scopedValue = ScopedValue.newInstance(); -
Structured Concurrency für verwandte Tasks
// ✅ Alle Tasks werden zusammen verwaltet try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { var f1 = scope.fork(task1); var f2 = scope.fork(task2); scope.join(); return combine(f1.resultNow(), f2.resultNow()); }
Wann Virtual Threads verwenden?
✅ Verwenden Sie Virtual Threads für:
- Web-Server (viele gleichzeitige Verbindungen)
- Mikro-Services (I/O-intensive)
- Datenbankabfragen
- Externe API-Aufrufe
- Datei-I/O
- Nachrichtenwarteschlangen
❌ Vermeiden Sie Virtual Threads für:
- CPU-gebundene Berechnungen
- Tight Loops ohne I/O
- Bereits optimierte reactive Code
- Sehr langlebige Threads
Migration von Platform zu Virtual Threads
// Vorher
ExecutorService oldExecutor = Executors.newFixedThreadPool(100);
// Nachher - einfacher Austausch!
ExecutorService newExecutor = Executors.newVirtualThreadPerTaskExecutor();
// Verwendung ist identisch
newExecutor.submit(() -> handleRequest());Fazit
Virtual Threads sind ein Game-Changer für Java-Anwendungen:
- Einfacherer Code als reaktive Programmierung
- Bessere Ressourcennutzung als Thread-Pools
- Höherer Durchsatz für I/O-intensive Apps
- Nahtlose Migration vom bestehenden Code
Ab Java 21 sind sie production-ready und sollten für die meisten Server-Anwendungen in Betracht gezogen werden.