Skip to content

SYSTEM Cited by 1 source

Java 21 Virtual Threads

Virtual threads (VTs) shipped as a preview in Java 19, stabilized in Java 21 via JEP 444, and are described as "lightweight threads that dramatically reduce the effort of writing, maintaining, and observing high-throughput concurrent applications" (per the JDK docs).

Model

A VT is not 1:1 with an OS thread. It is "a task that is scheduled to a fork-join thread pool" — see concepts/fork-join-pool. The VT multiplexes onto a pool of underlying OS worker threads ("carrier threads") via JVM-managed continuations:

  • When a VT does blocking I/O (or LockSupport.park, or waits on a Future / ReentrantLock / CompletableFuture), the runtime unmounts it from its carrier: the continuation saves the VT stack to the heap; the carrier is released back to the pool.
  • When the VT becomes runnable again, the scheduler mounts it on any available carrier and resumes execution from the saved continuation.

The default fork-join pool size is Runtime.getRuntime().availableProcessors() — typically equal to vCPU count on JVM-ergonomics-defaulted cloud VMs.

Pinning

VTs cannot unmount in certain JVM states. The most important class for Java 21:

"A VT will be pinned to the underlying OS thread if it performs a blocking operation while inside a synchronized block or method."JDK 21 core docs

While pinned, the VT holds its carrier OS thread for the entire block — exactly the anti-behavior VTs were supposed to fix. concepts/virtual-thread-pinning is the canonical wiki entry for this failure mode.

Pinning also occurs inside native-code frames that hold monitors, though the synchronized-block case is the most common.

Removal of pinning-during-synchronized (in progress)

The Netflix 2024-07-29 post flags the JDK-level fix: "This is a limitation in Java 21 and will be addressed in the future releases." Subsequent JEPs (JEP 491, targeted at a later JDK) describe removing the pinning-during-synchronized constraint, letting VTs unmount across synchronized the same way they do across ReentrantLock. Until this lands and is adopted widely, the operator-visible advice is: don't call blocking APIs inside synchronized, and watch out for library authors who do.

Observability in Java 21

  • jstack does not show VT stacks — it only iterates platform threads.
  • jcmd Thread.dump_to_file does include VTs (stack traces, IDs, states) but omits lock-owner/parking metadata in Java 21 — a named limitation being addressed.

See concepts/jcmd-thread-dump.

Tomcat + VT integration

Spring Boot's embedded Tomcat ships a VirtualThreadExecutor — a one-line config flag switches request handling from platform-thread-per-request to VT-per-request. This preserves Tomcat's blocking per-request model while (in theory) removing the cap that the platform thread-pool imposed.

Seen in

  • sources/2024-07-29-netflix-java-21-virtual-threads-dude-wheres-my-lock — Canonical wiki introduction. Production Netflix microservices on Java 21 + Spring Boot 3 + embedded Tomcat hung with closeWait pile-up due to 4 VTs pinned inside synchronized on the Brave span-finish path, exhausting all 4 carrier threads on a 4-vCPU instance. The post also names the JDK limitations (missing lock metadata in jcmd dumps; pinning-during- synchronized) and the long-term fix path (upstream JDK changes). Canonical Java-language-runtime instance of patterns/upstream-the-fix.
Last updated · 319 distilled / 1,201 read