Virtual Threads
Virtual threads, introduced as part of Project Loom in Java 19, represent a paradigm shift in how we approach concurrent programming in Java. This article explores how virtual threads drastically reduce overhead compared to traditional platform threads, with practical examples to demonstrate their efficiency.
Understanding the Thread Model Problem
For decades, Java’s concurrency model has been based on platform threads, which are direct mappings to operating system threads. While powerful, this model has significant limitations:
- Resource Intensity: Each platform thread consumes approximately 1MB of stack memory
- Scheduling Overhead: OS-level context switching is expensive
- Scalability Ceiling: Most applications hit performance issues when scaling beyond a few thousand threads
This creates a fundamental mismatch: while our programming model encourages thinking in terms of one thread per task, the implementation makes this prohibitively expensive at scale.
Virtual Threads: The Solution
Virtual threads solve this mismatch through an elegant abstraction. Here’s how they differ from platform threads:
Key Differences

How Virtual Threads Reduce Overhead
The magic of virtual threads happens through a technique called thread mounting and unmounting:
- When a virtual thread starts executing, it “mounts” onto a platform thread (called a carrier thread)
- If the virtual thread performs a blocking operation, it’s automatically unmounted
- The carrier thread is freed to execute other virtual threads
- When the blocking operation completes, the virtual thread is scheduled to run again on an available carrier thread
Virtual Thread Mounting/Unmounting Process

This mounting/unmounting process is what makes virtual threads so efficient:
- Memory Efficiency: Virtual threads use a fraction of the memory of platform threads
- CPU Efficiency: Carrier threads are never blocked waiting for I/O
- Simplified Programming Model: Developers can write straightforward sequential code that performs well at scale
Processing 10,000 Tasks Concurrently with Virtual Threads
Here’s a practical example of processing 10,000 tasks using virtual threads:
Processing 10,000 Tasks with Virtual Threads
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class VirtualThreadDemo {
public static void main(String[] args) {
int taskCount = 10_000;
// Measure execution time
Instant start = Instant.now();
try {
// Create a virtual thread per task executor
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
// Submit 10,000 tasks
for (int i = 0; i < taskCount; i++) {
final int taskId = i;
executor.submit(() -> {
// Simulate work with network I/O (e.g., HTTP request)
try {
// Simulate a task that blocks for I/O
Thread.sleep(200);
processTask(taskId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return null;
});
}
// For comparison, this would typically require only ~200ms with virtual threads
// but would take ~200s with a fixed thread pool of 100 platform threads
// Initiate an orderly shutdown
executor.shutdown();
// Wait for tasks to complete, with timeout
if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
Instant end = Instant.now();
System.out.printf("Processed %d tasks in %d ms%n",
taskCount,
Duration.between(start, end).toMillis());
}
private static void processTask(int taskId) {
// Actual business logic would go here
System.out.printf("Task %d executed by thread: %s%n",
taskId,
Thread.currentThread());
}
}
Comparative Performance
Let’s compare what would happen if we processed the same 10,000 tasks with platform threads versus virtual threads:
Platform vs Virtual Threads Performance

When to Use Virtual Threads
Virtual threads excel in IO-bound applications like:
- Web servers handling many concurrent connections
- Microservices making multiple downstream calls
- Applications performing database operations
- File processing systems
However, they may not improve performance for CPU-bound workloads where the bottleneck is computational power rather than waiting for IO.
Implementation Details: How Virtual Thread Continuations Work
Behind the scenes, virtual threads are implemented using a technique called continuations. When a virtual thread blocks:
- Its execution state (stack frames, local variables) is captured in a continuation object
- The carrier thread is released back to the platform thread pool
- When the blocking operation completes, the continuation is scheduled to resume
- When scheduled, the continuation restores the execution state and continues from exactly where it left off
Virtual Thread Implementation Architecture

Conclusion
Virtual threads represent a significant advancement in Java’s concurrency model. By decoupling the programming model (one task = one thread) from the implementation (efficient sharing of platform threads), Project Loom enables Java applications to handle unprecedented levels of concurrency with minimal overhead.
The beauty of virtual threads is that they require minimal changes to existing code – often just replacing thread pool creation code – while delivering substantial performance improvements for IO-bound applications.
As Java continues to evolve, virtual threads are poised to become the default approach for concurrent programming, enabling developers to write straightforward, maintainable code that scales effortlessly to handle millions of concurrent operations.
Leave a Reply