What are Virtual Threads?
Virtual threads are lightweight threads introduced in Java 21 (JEP 444) that enable high-throughput concurrent applications. They are managed by the JVM rather than the operating system, allowing you to create millions of threads efficiently.
Massive Scalability
Create millions of virtual threads instead of being limited to thousands of platform threads. Perfect for I/O-bound workloads.
Low Memory Footprint
Each virtual thread uses only ~1KB of memory compared to ~1MB for platform threads. Run more concurrent tasks with less RAM.
Simple Programming Model
Write straightforward blocking code without complex async patterns. The JVM handles the complexity of efficient scheduling.
Seamless Integration
Virtual threads work with existing Java code. Use familiar APIs like ExecutorService, locks, and blocking I/O operations.
Fast Context Switching
Context switching happens in user space, not kernel space. Much faster than OS-level thread switching.
Automatic Carrier Management
Virtual threads automatically mount and unmount from carrier threads. When blocked on I/O, the carrier is freed for other work.
When to Use Virtual Threads
Ideal Use Cases
- Web servers handling many concurrent requests
- Database-heavy applications with connection pools
- Microservices making multiple API calls
- File processing and batch operations
Not Recommended For
- CPU-intensive computations (use platform threads)
- Long-running synchronized blocks
- Native code that blocks in JNI
- Code using ThreadLocal for pooled resources
Evolution of Java Concurrency
Thread Architecture Comparison
Platform Threads (1:1)
Virtual Threads (M:N)
- • Each Java thread = one OS thread
- • ~1MB memory per thread
- • Blocked thread = wasted OS thread
- • Limited to ~4,000 threads
- • Expensive context switching
- • Many virtual threads share few carriers
- • ~1KB memory per thread
- • Blocked = unmount, carrier freed
- • Millions of threads possible
- • Fast user-space scheduling
Platform vs Virtual Threads Comparison
Memory Usage Visualization
Comparison for running 10,000 concurrent threads
Virtual threads use ~1000x less memory than platform threads
Before vs After Java 21
Creating a Thread
Simple thread creation for a background task
// Pre-Java 21: Platform ThreadThread thread = new Thread(() -> { System.out.println("Running!"); String result = fetchDataFromAPI(); System.out.println(result);});thread.start();// Each thread consumes ~1MB of memory// Limited to ~4,000 concurrent threads// Java 21: Virtual ThreadThread.startVirtualThread(() -> { System.out.println("Running!"); String result = fetchDataFromAPI(); System.out.println(result);});// Each thread consumes ~1KB of memory// Can create millions of threadsExecutor Service
Running 10,000 concurrent tasks
// Pre-Java 21: Fixed Thread PoolExecutorService executor = Executors.newFixedThreadPool(200);for (int i = 0; i < 10_000; i++) { executor.submit(() -> { Thread.sleep(Duration.ofSeconds(1)); return fetchData(); });}// Only 200 tasks run concurrently// 9,800 tasks waiting in queue// Total time: ~50 seconds// Java 21: Virtual Thread Per Tasktry (var executor = Executors .newVirtualThreadPerTaskExecutor()) { for (int i = 0; i < 10_000; i++) { executor.submit(() -> { Thread.sleep(Duration.ofSeconds(1)); return fetchData(); }); }}// All 10,000 tasks run concurrently// Total time: ~1 secondHTTP Server
Handling concurrent HTTP requests
// Pre-Java 21: Limited concurrencyvar server = HttpServer.create( new InetSocketAddress(8080), 0);server.setExecutor( Executors.newFixedThreadPool(200));server.createContext("/api", exchange -> { // Blocked thread = wasted resource String data = queryDatabase(); respond(exchange, data);});// Max ~200 concurrent requests// Java 21: Massive concurrencyvar server = HttpServer.create( new InetSocketAddress(8080), 0);server.setExecutor(Executors .newVirtualThreadPerTaskExecutor());server.createContext("/api", exchange -> { // Blocked? No problem! String data = queryDatabase(); respond(exchange, data);});// Handles 1M+ concurrent requestsParallel API Calls
Fetching data from multiple services
// Pre-Java 21: Complex async codeCompletableFuture<User> userFuture = CompletableFuture.supplyAsync( () -> fetchUser(id));CompletableFuture<Order> orderFuture = CompletableFuture.supplyAsync( () -> fetchOrder(id));CompletableFuture.allOf(userFuture, orderFuture) .thenAccept(v -> { User user = userFuture.join(); Order order = orderFuture.join(); process(user, order); });// Callback complexity, error handling is hard// Java 21: Simple, readable codetry (var scope = new StructuredTaskScope .ShutdownOnFailure()) { var user = scope.fork(() -> fetchUser(id)); var order = scope.fork(() -> fetchOrder(id)); scope.join(); scope.throwIfFailed(); process(user.get(), order.get());}// Clean, linear flow, easy error handlingKey Insight
Virtual threads let you write simple, blocking code while achieving the performance of complex async/reactive patterns. Same code style, 1000x more throughput.