Virtual Threads
Virtual threads are JVM user-mode threads scheduled onto a carrier pool, mounting and unmounting on blocking operations, so thread-per-request code scales to millions of tasks.
Definition
Virtual threads (Project Loom) are lightweight JVM threads managed by the runtime rather than the kernel. They mount on carrier platform threads and unmount when blocking on eligible operations — sockets, locks, sleeps — freeing the carrier for other work. Code keeps the synchronous thread-per-request shape while achieving scheduler-managed massive concurrency.
Context and scope
Java 21+ server workloads replacing async/reactive stacks for I/O-bound services. Pinned carriers are the key operational hazard.
Common misconceptions
- Virtual threads replace thread pools for CPU work — they shine on I/O-bound waiting; CPU-bound work still wants core-count platform threads
- Blocking is now free — eligible blocking unmounts the virtual thread; pinning on synchronized blocks or native frames still freezes the carrier
- Virtual threads make reactive frameworks obsolete — many use cases simplify; throughput constraints move to downstream pools and locks
Anti-patterns
- Calling synchronized-heavy or native-library code on virtual threads and pinning carriers under load
- Semaphores for resource limits ignored in favor of unbounded virtual thread creation
- Thread-locals holding expensive per-thread resources now instantiated per million virtual threads
Related concepts
Related examples
Authoritative source
What it is
Platform threads map one-to-one onto kernel threads and cost megabytes of stack each. Virtual threads break the mapping: the JVM schedules them onto a small carrier pool, parking them on blocking calls by saving their stack state. Thread-per-request servers stop choosing between clarity and scale.
How it works
Blocking operations are intercepted by the runtime: the virtual thread state is stored, the carrier is released, and resumption continues the stack later. Pinning defeats this — synchronized blocks that block while holding a monitor, or calls into native frames, cannot unmount and stall the carrier. Java mitigates synchronized pinning in later releases, but native frames remain.
When to use it
I/O-heavy request handlers, fan-out aggregators, and legacy blocking libraries that reactive wrappers never covered. Keep CPU-bound work on platform thread pools; use semaphores to bound access to scarce downstream resources, since virtual thread counts are effectively unbounded.
Pitfalls
Cheap threads invite unbounded task creation — queueing must move somewhere. Thread-local caches and pools sized per thread become per-million-thread liabilities. Pinning shows up as throughput cliffs under load: detect with JFR carrier pinning events, not by reading code.