How Java Virtual Threads Work
IntermediateVirtual threads, delivered in Java 21 via Project Loom, are lightweight threads managed by the JVM rather than the operating system. A traditional platform thread maps 1:1 to an OS thread and costs ~1MB of stack, so a server can run only a few thousand. Virtual threads are cheap — you can have millions. When a virtual thread blocks on I/O, the JVM unmounts it from its carrier (a real OS thread) and runs another, so the simple thread-per-request style finally scales without reactive complexity.
Think of virtual threads as tasks sharing a few real workers
Imagine a call center with 4 phone operators (carrier threads) but thousands of customers on hold (virtual threads). When one customer goes silent waiting for a database to respond, the operator sets them aside and picks up the next active customer. No operator sits idle waiting. You write code as if each customer has their own dedicated operator, but the JVM quietly shares a small pool of real workers behind the scenes.
Step by Step
Key Concepts
Platform vs Virtual Thread
A platform thread is a thin wrapper over an OS thread — expensive and limited in number. A virtual thread is scheduled by the JVM over a small pool of carriers, making it cheap to create by the million.
Carrier Thread
A real platform thread from a ForkJoinPool that actually executes virtual threads. Virtual threads mount and unmount from carriers as they run and block.
Pinning
A virtual thread can get "pinned" to its carrier (unable to unmount) inside a synchronized block or a native call. Heavy pinning defeats the benefit — prefer ReentrantLock over synchronized on hot blocking paths.
Structured Concurrency
A companion API (StructuredTaskScope) that treats a group of concurrent subtasks as one unit — if one fails or the caller is cancelled, the rest are cancelled too, preventing thread leaks.
Key Facts
- Virtual threads make the simple thread-per-request server model scale to huge concurrency, often removing the need for reactive frameworks in I/O-bound services.
- They do not make CPU-bound work faster — you still have only as many carriers as CPU cores for actual computation.
- Do not pool virtual threads. Create a new one per task; pooling them adds contention and defeats their cheapness.
Real-World Applications
A high-concurrency REST API
A service that mostly calls other services and a database is I/O-bound. Running each request on a virtual thread lets one instance handle tens of thousands of concurrent requests with plain blocking code — no callbacks, no reactive operators.
Fan-out aggregation
When one request must call ten downstream services, structured concurrency spins up ten virtual threads, waits for all, and cancels the rest if one fails — cleanly, with far less code than a CompletableFuture chain.
Frequently Asked Questions
Do virtual threads replace reactive programming?
For many I/O-bound workloads, yes — they give reactive-level scalability with simple, debuggable blocking code. Reactive stacks still help for streaming, backpressure, and composing async data flows, but virtual threads remove the main reason most teams reached for reactive: thread scarcity.
Are virtual threads faster than platform threads?
Not for raw CPU work — computation still needs real cores. Their advantage is concurrency: you can have millions of blocked virtual threads cheaply, so throughput on I/O-bound systems rises dramatically without exhausting memory.
What is thread pinning and why does it matter?
Pinning happens when a virtual thread cannot unmount from its carrier — inside a synchronized block or a native (JNI) call. If it blocks while pinned, it ties up a carrier. Replacing synchronized with ReentrantLock on blocking hot paths avoids it.
Which Java version do I need?
Virtual threads shipped as a final feature in Java 21 (LTS). Frameworks like Spring Boot 3.2+ and Helidon expose simple switches to run request handling on virtual threads.