Virtual Threads (Project Loom) — Complete Guide
In this tutorial, you will learn about Virtual Threads (Project Loom). We cover key concepts, practical examples, and best practices to help you master this topic.
The Problem with Traditional Threads
Java's threading model has served developers well for decades, but platform threads (also called OS threads) are a finite, expensive resource. Each platform thread consumes around 1 MB of stack memory and requires a costly OS syscall to create. A typical server can handle only a few thousand platform threads before performance degrades significantly. This limitation forced developers to use asynchronous programming patterns with callbacks, futures, or reactive frameworks, which introduced complexity and made code harder to read and debug.
Virtual threads solve this problem by decoupling the abstraction of a thread from the OS thread that runs it. A virtual thread is a lightweight thread managed by the JVM rather than the operating system. Millions of virtual threads can exist in a single application, each consuming only a few hundred bytes. They enable writing simple synchronous code that scales like asynchronous code, combining the best of both worlds.
flowchart LR
A[Concurrency Need] --> B[Platform Threads
1:1 with OS]
A --> C[Virtual Threads
M:1 with OS]
B --> D[Limited ~1MB stack
Thousands max]
C --> E[Tiny ~KB stack
Millions possible]
E --> F[Simple synchronous code
Async scalability]
What Are Virtual Threads?
A virtual thread is an instance of java.lang.Thread that is not tied to a specific OS thread. The JVM schedules virtual threads onto a small pool of carrier threads (platform threads). When a virtual thread blocks on an I/O operation, the JVM unmounts it from the carrier thread and mounts another virtual thread, allowing the carrier thread to stay busy. This mount-unmount cycle happens transparently.
Creating Virtual Threads
Java 21 provides several ways to create virtual threads.
// Using Thread.ofVirtual()
Thread vThread = Thread.ofVirtual()
.name("my-virtual-thread")
.start(() -> {
System.out.println("Hello from " + Thread.currentThread());
});
// Using Thread.startVirtualThread()
Thread vThread2 = Thread.startVirtualThread(() -> {
System.out.println("Quick virtual thread");
});
// Using Executors.newVirtualThreadPerTaskExecutor()
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> {
System.out.println("Virtual thread from executor");
});
}
vThread.join();
vThread2.join();
Output:
Hello from VirtualThread[my-virtual-thread]/runnable@ForkJoinPool-1-worker-1
Quick virtual thread
Virtual thread from executor
Checking if a Thread is Virtual
Thread t = Thread.ofVirtual().start(() -> {});
System.out.println("Is virtual? " + t.isVirtual());
t.join();
Output:
Is virtual? true
Virtual vs Platform Threads
The key differences between platform and virtual threads affect how you design concurrent applications.
| Aspect | Platform Thread | Virtual Thread |
|---|---|---|
| Creation cost | Expensive (syscall) | Cheap (JVM-managed) |
| Stack size | ~1 MB | ~10 KB (grows as needed) |
| Max count | Thousands | Millions |
| Tied to OS thread | Yes | No (mounts/unmounts) |
| CPU-bound tasks | Good | Good (same carrier pool) |
| I/O-bound tasks | Wastes carrier thread | Excellent (unmounts on block) |
When to Use Virtual Threads
Virtual threads excel at I/O-bound workloads where threads spend most of their time waiting. They are ideal for web servers, database access, REST API calls, and file operations.
// Simulating many I/O operations with virtual threads
var start = System.nanoTime();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
try {
Thread.sleep(100); // Simulate I/O
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
});
}
var end = System.nanoTime();
System.out.println("Completed 10,000 tasks in " +
(end - start) / 1_000_000 + " ms");
Output (approximate):
Completed 10,000 tasks in 250 ms
The same workload with platform threads would either fail with an out-of-memory error or take significantly longer due to thread creation overhead and context switching.
Structured Concurrency
Structured concurrency is a companion feature that treats groups of related tasks as a single Unit of Work. It ensures that if a subtask fails, all related subtasks are cancelled, preventing thread leaks and orphaned work.
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> user = scope.fork(() -> fetchUser(1));
Future<String> orders = scope.fork(() -> fetchOrders(1));
scope.join(); // Wait for all tasks
scope.throwIfFailed(); // Propagate any failure
System.out.println(user.resultNow());
System.out.println(orders.resultNow());
}
Output:
User{id=1, name='Alice'}
Order{id=101, total=250.00}
Common Mistakes
1. Synchronized Blocks Pin Carrier Threads
If a virtual thread enters a synchronized block or method, it pins the carrier thread, preventing unmounting during blocking operations.
// Problematic: synchronized blocks pin carrier threads
private final Object lock = new Object();
void doWork() {
synchronized (lock) {
try {
Thread.sleep(1000); // Carrier thread is pinned!
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Prefer ReentrantLock instead, which does not pin carrier threads.
2. Thread Pool Abuse with Virtual Threads
Using a thread pool with virtual threads defeats their purpose. Virtual threads should be created without pooling.
// Wrong: pooling virtual threads
ExecutorService pool = Executors.newFixedThreadPool(100);
for (int i = 0; i < 10_000; i++) {
pool.submit(task); // Limits scalability
}
// Correct: per-task executor
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
executor.submit(task);
}
}
3. ThreadLocal Memory Leaks
Virtual threads can outlive their intended scope. Using ThreadLocal with virtual threads can cause memory leaks because the JVM may cache virtual threads.
// Risky with virtual threads
ThreadLocal<byte[]> largeData = new ThreadLocal<>();
largeData.set(new byte[1024 * 1024]); // May not be GC'd promptly
4. Assuming Platform Thread Performance
Virtual threads are not faster for CPU-bound work. They share carrier threads from a fork-join pool (typically matching CPU core count).
// This will NOT run faster with virtual threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100; i++) {
executor.submit(() -> computePi(10_000)); // CPU-bound
}
}
5. Ignoring InterruptedException
Virtual threads respect interruption. Swallowing InterruptedException can leave threads in inconsistent states.
6. Mixing Platform and Virtual Threads in ThreadLocal
Values stored in ThreadLocal on a platform thread are not visible to virtual threads, and vice versa.
Practice Questions
- What is the primary advantage of virtual threads over platform threads for I/O-bound applications?
- How does the JVM handle a blocking operation inside a virtual thread?
- What happens when a virtual thread enters a synchronized block that performs blocking I/O?
- Why is thread pooling unnecessary (and counterproductive) for virtual threads?
- Describe a scenario where platform threads would outperform virtual threads.
Challenge: Write a program that creates 100,000 virtual threads, each making a simulated HTTP request (using Thread.sleep to simulate latency). Measure the total execution time. Then implement the same with platform threads and compare.
FAQ
Mini Project: High-Performance Web Scraper
Build a web scraper that fetches 1,000 URLs concurrently using virtual threads. The scraper should:
- Accept a list of URLs and fetch each in a separate virtual thread
- Parse HTML title tags from each response
- Handle timeouts and failures gracefully
- Report progress every 100 requests
- Aggregate results into a map of URL to title
Use HttpClient.newHttpClient() (which works naturally with virtual threads) and Executors.newVirtualThreadPerTaskExecutor().
What's Next
You have mastered virtual threads, the most significant concurrency improvement in modern Java. In the next lesson, you will learn about the Java Platform Module System (JPMS) for building modular applications.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro