← Concepts

阅读中文版本

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.

concurrency

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

https://openjdk.org/jeps/444

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.