Virtual Threads
Virtual threads (since Java 21, JEP 444) are lightweight threads managed by the JVM rather than the OS. They enable high-throughput concurrent applications — especially I/O-bound workloads — without the overhead of platform threads.
Platform Threads vs Virtual Threads
| Feature | Platform Thread | Virtual Thread |
|---|---|---|
| Managed by | OS | JVM |
| Memory overhead | ~1 MB stack | ~few KB |
| Max practical count | Thousands | Millions |
| Best for | CPU-bound | I/O-bound |
| Creation cost | Expensive | Cheap |
Creating Virtual Threads
// Java 21+
Thread vThread = Thread.startVirtualThread(() -> {
System.out.println("Running in virtual thread: " + Thread.currentThread());
});
vThread.join();
Using Executors
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
} // executor closes automatically
Each task gets its own virtual thread — no pooling needed.
Thread.ofVirtual()
ThreadFactory factory = Thread.ofVirtual().name("worker-", 0).factory();
Thread t = factory.newThread(() -> System.out.println("Hello"));
t.start();
Why Virtual Threads Matter
Traditional thread-per-request models hit limits with platform threads:
// ❌ 10,000 platform threads — likely OutOfMemoryError or severe slowdown
try (var executor = Executors.newFixedThreadPool(10_000)) { ... }
// ✅ 10,000 virtual threads — feasible
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { ... }
Virtual threads shine when tasks block on I/O (HTTP calls, database queries, file reads). The JVM parks virtual threads during blocking, freeing the underlying carrier thread.
Pinning — Important Caveat
A virtual thread is pinned to its carrier platform thread when:
- Running code inside a
synchronizedblock/method - Running native code or foreign function calls
Pinned virtual threads cannot be unmounted during blocking, reducing scalability:
// ❌ May pin virtual thread
synchronized (lock) {
socket.read(); // blocking I/O while pinned
}
// ✅ Prefer ReentrantLock
lock.lock();
try {
socket.read();
} finally {
lock.unlock();
}
Structured Concurrency (Preview)
Groups related tasks with a clear lifetime:
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<String> user = scope.fork(() -> fetchUser());
Subtask<Integer> order = scope.fork(() -> fetchOrderCount());
scope.join();
scope.throwIfFailed();
return new Dashboard(user.get(), order.get());
}
Migration Tips
| Before | After |
|---|---|
| Fixed thread pool for I/O | newVirtualThreadPerTaskExecutor() |
@Async with thread pool |
Virtual thread executor |
| Reactive streams for concurrency | Simple blocking code on virtual threads |
Do not pool virtual threads — create one per task.
Best Practices
- Use virtual threads for I/O-bound, thread-per-task workloads
- Avoid pooling virtual threads
- Replace
synchronizedwithReentrantLockin hot paths to prevent pinning - Do not use virtual threads for CPU-intensive tasks — platform threads are better
- Requires Java 21+ (production-ready since Java 21)