🚀AdvancedModern Java

Virtual Threads

Use Java 21 virtual threads for massive concurrency with blocking code, without the overhead of OS threads.

Overview

Virtual threads (Project Loom, finalized in Java 21) are lightweight threads managed by the JVM rather than the OS. Unlike platform threads (which are 1:1 with OS threads and cost ~1MB each), virtual threads are cheap to create and block without consuming OS resources. You can run millions of them simultaneously. The key insight: virtual threads are ideal for blocking I/O code. When a virtual thread blocks (on a socket read, database call, or sleep), the JVM parks it and frees the carrier thread for other work. No thread is wasted waiting. Virtual threads do NOT improve CPU-bound code. For parallel computation, use ForkJoinPool or parallel streams as before. Virtual threads are for high-concurrency I/O-bound work.

Key Points

  • •Thread.ofVirtual().start(runnable) creates and starts a virtual thread
  • •Thread.ofVirtual().unstarted(runnable) creates but does not start
  • •Executors.newVirtualThreadPerTaskExecutor() creates an executor that uses one virtual thread per task
  • •Virtual threads are created by the JVM and scheduled on carrier threads (platform threads from ForkJoinPool)
  • •Blocking on I/O, sleep, or locks unmounts the virtual thread from its carrier without blocking the carrier
  • •Platform threads are still the carriers; the JVM multiplexes virtual threads onto them
  • •Virtual threads cannot be pooled; create a new one per task (they are cheap)
  • •synchronized blocks can pin the virtual thread to its carrier; prefer ReentrantLock inside virtual threads
  • •Structured concurrency (Java 21+) provides scope-based lifetime management for groups of virtual threads

Examples

Creating virtual threads vs platform threads

public class Main {
    public static void main(String[] args) throws InterruptedException {
        // Platform thread (traditional)
        Thread platformThread = Thread.ofPlatform().start(() ->
            System.out.println("Platform thread: " + Thread.currentThread().isVirtual()));

        // Virtual thread
        Thread virtualThread = Thread.ofVirtual().start(() ->
            System.out.println("Virtual thread: " + Thread.currentThread().isVirtual()));

        platformThread.join();
        virtualThread.join();
    }
}

isVirtual() distinguishes virtual from platform threads. Virtual threads behave exactly like platform threads from the API perspective; the difference is in their lifecycle and scheduling.

Spawning 100,000 virtual threads concurrently

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        int taskCount = 100_000;
        AtomicInteger completed = new AtomicInteger(0);

        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < taskCount; i++) {
                executor.submit(() -> {
                    // Simulate I/O wait
                    try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
                    completed.incrementAndGet();
                });
            }
        } // auto-shutdown and await termination

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

Creating 100,000 platform threads would consume ~100GB of memory and fail. With virtual threads, this runs in seconds with minimal memory. The try-with-resources on ExecutorService shuts down and awaits completion.

Migrating a blocking HTTP service to virtual threads

import java.util.concurrent.*;

// Simulates a blocking service call (e.g., database, HTTP)
class OrderService {
    String getOrder(int id) {
        try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        return "Order#" + id;
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        OrderService service = new OrderService();
        int requests = 200;

        long start = System.currentTimeMillis();

        // Virtual thread per request: no thread pool sizing needed
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            var futures = new java.util.ArrayList<Future<String>>();
            for (int i = 1; i <= requests; i++) {
                final int id = i;
                futures.add(executor.submit(() -> service.getOrder(id)));
            }
            for (Future<String> f : futures) {
                f.get(); // just drain results
            }
        }

        System.out.println(requests + " requests in " + (System.currentTimeMillis() - start) + "ms");
        // Should be ~50ms, not 200 * 50ms, because blocking is free with virtual threads
    }
}

With platform threads and a fixed pool (e.g., 10 threads), 200 requests would take at minimum 200 * 50ms / 10 = 1000ms. With virtual threads, all 200 run concurrently and complete in ~50ms regardless of blocking time.

Common Mistakes

  • Using virtual threads for CPU-bound work. Virtual threads do not make CPU work parallel; they only help with blocking I/O. For CPU parallelism, use ForkJoinPool or parallel streams.
  • Pooling virtual threads. Virtual threads are cheap; a "virtual thread pool" is an anti-pattern. Use newVirtualThreadPerTaskExecutor() and let the JVM manage them.
  • Using synchronized blocks that hold long-running I/O. synchronized pins the virtual thread to its carrier, blocking that carrier even during I/O. Use ReentrantLock for locks held across blocking operations.
  • Assuming virtual threads are always faster. Virtual threads reduce thread overhead for I/O concurrency. Sequential code or purely CPU-bound code does not benefit.

Note

You can use virtual threads transparently with existing blocking I/O APIs (JDBC, HTTP clients, file reads). No code changes are needed except switching the executor. This is the key advantage: you do not need to rewrite blocking code as async callbacks to get high concurrency.

Exercises

Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.

Loading editor…

Completed: 0 / 2 exercises

Knowledge Check

Question 1 of 4

What happens when a virtual thread blocks on I/O?