Virtual threads (Project Loom, finalized as JEP 444 in Java 21) are lightweight, JVM-managed threads that make the classic thread-per-request style scale to millions of concurrent tasks. They keep the blocking, sequential programming model you already know while delivering the throughput people previously reached for reactive frameworks to get. The catch is understanding what they are not: they are not faster threads, and they are not for CPU work.
Cheap threads, the same API
A platform thread is a thin wrapper over an OS thread — each costs a megabyte-scale stack and a kernel scheduling slot, so you can realistically run a few thousand. A virtual thread is a Thread whose stack lives on the heap and is scheduled by the JVM, not the OS. Creation is microsecond-cheap and a single JVM can hold millions of them. Crucially they are still java.lang.Thread: the same Thread API, the same try/catch, the same debugger and stack traces.
Thread.startVirtualThread(() -> handle(request));
try (var exec = Executors.newVirtualThreadPerTaskExecutor()) {
requests.forEach(r -> exec.submit(() -> handle(r)));
}
M:N scheduling onto carriers
The JVM multiplexes many virtual threads onto a small pool of platform carrier threads (a dedicated ForkJoinPool, sized to the CPU count by default). A virtual thread runs by mounting a carrier; when it hits a blocking call (socket read, sleep, lock), it unmounts — its continuation/stack is parked on the heap and the carrier is freed to run another virtual thread.
VT1 VT2 VT3 ... VTn (millions, stacks on heap)
\ | / |
mount/unmount on block
/ | \ \
[Carrier1][Carrier2][Carrier3] (ForkJoinPool, ~#CPUs)
| | |
OS thread OS thread OS threadStructured concurrency and scoped values
Loom also ships StructuredTaskScope (structured concurrency) — tasks forked in a scope are joined and error-handled as a unit, so a failure or cancellation cleanly propagates instead of leaking orphan threads. ScopedValue replaces ThreadLocal for sharing immutable context: it is bounded to a dynamic scope, cheap, and safely inheritable by forked subtasks — which matters when you create millions of threads.