javaadvanced

Virtual Threads — Practical Patterns

Use Java 21 virtual threads for high-concurrency I/O: per-request threads, structured scopes, and limits.

java
import java.time.*;
import java.util.concurrent.*;
import java.util.stream.*;
import java.util.List;

public class VirtualThreadPatterns {

    // 1. Simple virtual thread
    static void basic() throws InterruptedException {
        Thread vt = Thread.ofVirtual().name("worker-1").start(() -> {
            System.out.println(Thread.currentThread());
        });
        vt.join();
    }

    // 2. Virtual thread executor — one thread per task
    static void executorPerTask() throws Exception {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            List<Future<String>> futures = IntStream.range(0, 10_000)
                .mapToObj(i -> executor.submit(() -> {
                    Thread.sleep(Duration.ofMillis(100)); // I/O sim
                    return "result-" + i;
                }))
                .toList();

            long completed = futures.stream()
                .map(f -> { try { return f.get(); } catch (Exception e) { return null; } })
                .filter(r -> r != null)
                .count();

            System.out.println("Completed: " + completed);
        }
    }

    // 3. Bounded virtual threads (limit concurrency)
    static void bounded() throws Exception {
        Semaphore limiter = new Semaphore(100); // max 100 concurrent

        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            IntStream.range(0, 1000).forEach(i ->
                executor.submit(() -> {
                    try {
                        limiter.acquire();
                        try {
                            Thread.sleep(50); // simulate I/O
                            return "done-" + i;
                        } finally {
                            limiter.release();
                        }
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        return "interrupted-" + i;
                    }
                }));
        }
        System.out.println("All bounded tasks done");
    }

    // 4. HTTP server pattern (one VT per request)
    static void serverPattern() throws Exception {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            // Simulate incoming requests
            for (int i = 0; i < 100; i++) {
                final int reqId = i;
                executor.submit(() -> handleRequest(reqId));
            }
        }
    }

    static String handleRequest(int id) {
        try {
            // Each request gets its own virtual thread
            String dbResult = queryDb(id);
            String cacheResult = queryCache(id);
            return dbResult + " | " + cacheResult;
        } catch (Exception e) {
            return "error: " + e.getMessage();
        }
    }

    static String queryDb(int id) throws InterruptedException {
        Thread.sleep(50); // simulate DB
        return "db-" + id;
    }

    static String queryCache(int id) throws InterruptedException {
        Thread.sleep(10); // simulate cache
        return "cache-" + id;
    }

    // 5. ThreadLocal alternative: ScopedValue (preview)
    // static final ScopedValue<String> USER = ScopedValue.newInstance();
    // ScopedValue.where(USER, "alice").run(() -> {
    //     System.out.println(USER.get()); // alice
    // });

    public static void main(String[] args) throws Exception {
        long start = System.nanoTime();

        basic();
        executorPerTask();
        bounded();
        serverPattern();

        System.out.printf("Total: %d ms%n", (System.nanoTime() - start) / 1_000_000);
    }
}

Use Cases

  • High-concurrency I/O-bound servers
  • Replacing thread pools for blocking workloads
  • Scalable microservice request handling

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.