RU | EN | DE

Virtual Threads (Project Loom)

Was sind Virtual Threads?

Virtual Threads sind leichtgewichtige Threads, die von der JVM verwaltet werden, nicht vom Betriebssystem.

Hauptvorteile:

  1. Extrem leichtgewichtig – Millionen können gleichzeitig existieren
  2. JVM-verwaltet – kein 1:1-Mapping zu OS-Threads
  3. Carrier Threads – Virtual Threads werden auf Platform Threads ausgeführt
  4. 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-1

Carrier 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-Kerne

Wie es funktioniert:

  1. Virtual Thread wird erstellt und einer Aufgabe zugewiesen
  2. Scheduler ordnet Virtual Thread einem freien Carrier Thread zu
  3. Bei Blockierung (I/O, sleep) wird Virtual Thread unmounted
  4. Carrier Thread kann einen anderen Virtual Thread ausführen
  5. 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:

  1. 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();
}
  1. 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 Threads

Performance-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

  1. Virtual Threads für I/O-intensive Aufgaben verwenden

    // ✅ Gut für Virtual Threads
    executor.submit(() -> readFromFile());
    executor.submit(() -> callExternalAPI());
    executor.submit(() -> queryDatabase());
  2. Platform Threads für CPU-intensive Aufgaben

    // ✅ Besser mit Platform Threads
    ForkJoinPool.commonPool().submit(() -> complexCalculation());
  3. Pinning vermeiden (vor JDK 24)

    // ❌ Schlecht
    synchronized(lock) { blockingOperation(); }
     
    // ✅ Gut
    ReentrantLock lock = new ReentrantLock();
    lock.lock();
    try { blockingOperation(); } finally { lock.unlock(); }
  4. 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();
  5. 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.