Comprehensive Guide to Virtual Threads (Project Loom) in Java
Target Java Versions:
- Preview: Java 19 (JEP 425), Java 20 (2nd preview)
- Final / Standard: Java 21 – JEP 444: Virtual ThreadsVirtual Threads are the most important Java concurrency upgrade since Java 1.0.
They make millions of threads possible, simplify concurrent programming, and eliminate the classic “thread-per-request is too expensive” limitation.
1. What Are Virtual Threads?
Virtual Threads are lightweight threads managed by the JVM, not the OS.
They provide the same Thread API and semantics as platform (OS) threads, but:
- Are extremely cheap to create (thousands to millions)
- Consume only a few KB of memory each
- Park (block) without blocking OS threads
- Scale synchronous code like asynchronous frameworks
Key idea:
A virtual thread runs on top of a pool of OS threads (“carrier threads”).
This allows traditional blocking code to scale like asynchronous code.
2. Why Virtual Threads? (Motivation)
Before virtual threads:
- Creating OS threads (~1 MB stack each) was expensive
- Java servers couldn’t scale thread-per-request
- Async frameworks using callbacks or CompletableFutures were required
- Blocking I/O consumed real OS threads → limited concurrency (~hundreds to thousands max)
With virtual threads:
- Blocking a virtual thread does not block an OS thread
- Thread-per-request architecture becomes practical
- You can write simple synchronous code with the scalability of async
3. How Virtual Threads Work
Virtual threads:
- Are scheduled by the JVM, not the OS
- Use continuations (stack frames are heap-allocated)
- Park (on blocking calls) and unpark without occupying a carrier thread
Whenever a virtual thread blocks on I/O, the JVM:
- Saves (“pinning” aside) its continuation
- Unmounts it from the OS carrier thread
- Frees the carrier thread for other virtual threads
- Later resumes the virtual thread on any available carrier
This makes blocking cheap.
4. Creating Virtual Threads
4.1. Using Thread.startVirtualThread()
Thread t = Thread.startVirtualThread(() -> {
System.out.println("Running in virtual thread: " + Thread.currentThread());
});
4.2. Using Thread Builder
Thread t = Thread.ofVirtual().start(() -> {
// business logic
});
4.3. Using ExecutorService (Recommended)
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
try (executor) {
executor.submit(() -> {
Thread.sleep(1000);
return "done";
});
}
This executor:
- Creates a new virtual thread per submitted task
- Avoids thread pools (pooling threads is unnecessary)
5. Virtual Threads vs Platform Threads
| Feature | Platform Thread (OS Thread) | Virtual Thread (JVM) |
|---|---|---|
| Stack size | ~1 MB | Small, grows dynamically |
| Creation cost | High | Very low |
| Max count | Thousands | Millions |
| Blocking | Blocks OS thread | Parks → frees carrier |
| Scheduling | OS kernel | JVM |
| Goal | CPU-bound | I/O-bound & concurrency-heavy |
Platform threads are still needed for CPU-intensive tasks.
Virtual threads are ideal for I/O-bound tasks (servers, APIs, DB calls, etc.).
6. Typical Virtual Thread Usage: Server Code
Traditional blocking handler:
void handle(Socket socket) throws Exception {
try (socket) {
var reader = socket.getInputStream();
var writer = socket.getOutputStream();
String request = readRequest(reader); // blocking
String response = handleRequest(request);
writer.write(response.getBytes()); // blocking
}
}
With virtual threads:
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
while (true) {
Socket socket = serverSocket.accept(); // blocking OK
executor.submit(() -> handle(socket)); // one virtual thread per request
}
No async APIs needed—blocking is cheap.
7. Performance Characteristics
7.1. Fast to Start
Millions of virtual threads can be created quickly:
for (int i = 0; i < 1_000_000; i++) {
Thread.ofVirtual().start(() -> {
// cheap
});
}
7.2. Ideal for I/O-Bound Work
Because virtual threads park on blocking:
Socket.read()Socket.write()- JDBC operations
- File I/O (as long as supported)
Result: I/O concurrency without async code.
7.3. Not a Silver Bullet for CPU-Bound Work
CPU-heavy code still requires platform threads or a fixed-size pool.
8. Structured Concurrency (Optional Complement)
Virtual threads work perfectly with Structured Concurrency (incubator API since Java 19–21).
Example using StructuredTaskScope:
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> user = scope.fork(() -> fetchUser());
Future<String> orders = scope.fork(() -> fetchOrders());
scope.join(); // waits for both
scope.throwIfFailed();
return user.result() + orders.result();
}
Makes concurrent code simpler and more readable.
9. Pinning: Important Concept
Most blocking operations unmount the virtual thread from its carrier thread.
But some operations pin the carrier thread, preventing reuse.
Pinned when:
- Entering a synchronized block
- Executing code in a foreign/native method (JNI)
Example (pinning):
synchronized(lock) {
Thread.sleep(1000); // pin: prevents unmounting
}
Rule of thumb:
- Avoid long blocking operations inside
synchronized - Prefer
ReentrantLock— does not pin
Example (non-pinning):
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
Thread.sleep(1000); // safe
} finally {
lock.unlock();
}
10. Debugging & Stack Traces
Virtual threads have full stack traces:
Thread.ofVirtual().start(() -> {
throw new RuntimeException("Boom");
});
Stack traces behave like normal threads.
Also works with:
- Thread dumps
- Debuggers (IDE support required)
- Profilers (sampling profilers recommended)
11. Integration with Frameworks
Framework support growing rapidly:
Fully compatible (blocking-friendly):
- Spring Framework 6.1+ / Spring Boot 3.2+
- Tomcat (recent versions)
- Jetty (recent versions)
- Helidon Nima
- Micronaut (experimental)
- Quarkus (experimental)
- Netty (for virtual thread per connection patterns)
JDBC (key!)
Most JDBC drivers now support proper unmounting on blocking I/O.
12. Limitations & Caveats
12.1. Pinning in synchronized blocks
Avoid synchronized + blocking.
12.2. Native I/O may pin
Depending on implementation.
12.3. CPU-bound tasks not magically faster
Virtual threads do not increase pure CPU throughput.
12.4. Use structured concurrency for task management
To avoid orphaned threads.
12.5. Legacy libraries may accidentally pin
If using JNI or synchronized I/O.
13. Best Practices
✔ Prefer virtual threads for:
- Network servers
- API endpoints
- Database access
- File I/O
- High-concurrency applications
✔ Avoid:
- Blocking inside synchronized
- Fine-grained locks
- JNI for blocking operations
✔ Combine with:
- Structured concurrency
ReentrantLockinstead ofsynchronized- Timeouts on blocking operations
14. Virtual Threads vs CompletableFuture / Async
| Topic | Virtual Threads | CompletableFuture / Async |
|---|---|---|
| Style | Synchronous | Asynchronous |
| Readability | High | Often lower |
| Debugging | Easy | Hard |
| Concurrency | Very high | Extremely high |
| Error handling | Simple | Often complicated |
| Learning curve | Low | High |
| When ideal? | I/O-bound concurrency | CPU-intensive batching or event loops |
Virtual threads allow you to keep synchronous style while achieving async performance.
15. Typical Interview Questions
Q1. What problem do virtual threads solve?
A: They make the thread-per-request model scalable by making blocking cheap.
Q2. How do virtual threads differ from platform threads?
- Lightweight
- Millions instead of thousands
- JVM-scheduled
- Blocking does not block OS threads
Q3. What is pinning?
A: When a virtual thread holds onto its carrier thread due to synchronized or JNI blocking.
Q4. Are virtual threads good for CPU-heavy tasks?
A: No — use platform threads or bounded pools for CPU-bound work.
Q5. Do virtual threads eliminate the need for async frameworks?
A: In many cases yes, because synchronous blocking code now scales well.
Q6. How do virtual threads help servers?
A: Enables one-thread-per-request model with millions of concurrent requests.
16. Summary
Virtual threads are a foundational shift in Java:
- Finalized in Java 21
- Allow millions of lightweight threads
- Make blocking I/O cheap
- Enable scalable synchronous programming
- Reduce need for async frameworks
- Perfect for modern server applications
Combined with structured concurrency, they bring Java concurrency into a new era.
This guide gives you the depth needed for interviews, real-world usage, and production readiness.