JEP 444 finalized virtual threads as a production feature in JDK 21, and the platform has been on JDK 21+ since the March 2024 upgrade. This release closes the second-order question : how should the runtime use virtual threads systematically, rather than ad-hoc per call-site. The answer ships as VirtualThreadExecutor — a single platform-managed executor that every async path now routes through.
What's wrong with raw newVirtualThreadPerTaskExecutor()
The JDK API is correct but bare. A call site that writes Executors.newVirtualThreadPerTaskExecutor() gets a working executor and four problems for free :
- No graceful shutdown. Closing the executor at JVM shutdown either drops in-flight work silently or hangs the shutdown waiting for tasks that nobody is tracking.
- No cancellation propagation. A cancellation upstream does not flow into the executor's pending submissions ; orphaned tasks accumulate.
- No observability. Each call-site executor has its own identity ; the platform's running-tasks dashboard cannot enumerate them, JFR cannot tag them, JMX cannot count them.
- No shutdown ordering. Async work needs to drain before the database pools tear down — otherwise the last virtual threads die holding connections that have already been forcibly closed. Per-site executors have no shared ordering mechanism.
What VirtualThreadExecutor adds on top
- One supervised executor.
VirtualThreadExecutor.EXECUTORis the singular entry point for "I need a virtual thread." EveryCompletableFuture, every async dispatcher, every fire-and-forget worker submits here. Counting the platform's virtual-thread usage becomes "ask the executor." - Graceful shutdown built in. The executor registers itself with
VMShutdownManager; on JVM stop the manager drains it with a bounded timeout, surfaces the unfinished-task count to operators, and only then proceeds to tear down the database pools. - Cancellation that propagates. Submission returns a tracked handle ; cancelling the handle cancels the virtual thread cleanly, with the standard JDK
InterruptedExceptionpath. No orphaned work. - Observability through JFR and JMX. Every submission emits a JFR event ; the running-task count is a JMX gauge ; long-running virtual threads surface in the platform's
jvm.running_taskstable the same way platform threads do.
Where it lands inside the platform
The migration is single-line at each call site — newVirtualThreadPerTaskExecutor() → VirtualThreadExecutor.EXECUTOR — applied across ~50 sites in this release and growing. The first beneficiaries :
- JDBC pool tier-3 connection creation. New connections materialise on the virtual-thread executor, so pool growth does not block on platform threads. Three thousand pools growing simultaneously cost kilobytes of stack each rather than megabytes.
- AI agent parallel tool execution. Multi-tool turns dispatch tools concurrently on virtual threads ; the executor's cancellation surface lets an agent
cancel()cleanly mid-turn. - Microservice gRPC client calls. Each outbound call to the eighteen-service mesh is a virtual thread ; the executor handles back-pressure when downstream services slow.
- Server-Sent Events streaming. Each active SSE connection holds a virtual thread for the duration of the stream ; thousands of concurrent SSE clients are sustainable without exhausting the OS thread budget.
Virtual threads stop being a per-call-site decision and become a platform property — opt-in is the absence of VirtualThreadExecutor, not its presence. The shutdown-ordering work that landed in the same window (Phase 3 of the shutdown-order refactor, January 2026) closes the operational loop : the executor knows when to drain, the database pools know when to wait, the JVM exits cleanly even under deep async load.