跪拜 Guibai
← All articles
Java

Virtual Threads Are Production-Ready: A Practical Guide from JDK 21 to Spring Boot

By 唐青枫 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

For any Java developer maintaining a traditional synchronous stack — Spring MVC, JDBC, blocking HTTP clients — virtual threads are the most practical path to higher concurrency without a reactive rewrite. The feature is stable in JDK 21, and JDK 24 removes the last major gotcha. This guide consolidates the patterns, pitfalls, and production considerations that teams need right now.

Summary

Virtual threads, delivered by Project Loom, are no longer a preview feature. JDK 21 made them a permanent part of the Java platform, and JDK 24 further eliminates the most painful pinning issue with `synchronized`. This practical guide covers the full spectrum: how virtual threads differ from platform threads, the correct API to use (`Executors.newVirtualThreadPerTaskExecutor()`), and why they are a game-changer for I/O-bound applications.

The guide demonstrates concrete patterns: concurrent HTTP aggregation with `HttpClient`, handling 10,000 blocking tasks without 10,000 OS threads, and integrating virtual threads into Spring Boot with a single configuration property. It also addresses the hard parts — virtual threads do not make CPU-bound tasks faster, they do not eliminate the need for database connection pools, and they require careful handling of `ThreadLocal` and `synchronized` (especially on JDK 21-23).

For teams running traditional Spring MVC + JDBC stacks, virtual threads offer a path to higher throughput without rewriting code in a reactive style. The guide includes a complete Spring Boot demo, monitoring tips with `jcmd` and JFR, and a clear comparison with WebFlux. The bottom line: virtual threads let you keep your synchronous, blocking code while dramatically increasing concurrency capacity.

Takeaways
Virtual threads are a stable feature in JDK 21, with significant improvements to `synchronized` pinning in JDK 24 (JEP 491).
Virtual threads do not make individual tasks faster; they increase throughput by allowing many more tasks to be in a blocking state simultaneously.
The recommended API for production is `Executors.newVirtualThreadPerTaskExecutor()`, which creates one virtual thread per task.
Virtual threads are daemon threads — always use `join()` or executor shutdown in standalone demos to prevent premature JVM exit.
Database connection pools (e.g., HikariCP) are still required; virtual threads reduce the cost of waiting for a connection, not the need for one.
`ThreadLocal` works with virtual threads, but caching large objects per thread is dangerous because virtual threads are numerous and short-lived.
On JDK 21-23, `synchronized` blocks can pin a virtual thread to its carrier thread, reducing throughput. JDK 24 largely eliminates this.
CPU-bound tasks should still use a bounded platform thread pool sized to the number of cores.
Spring Boot enables virtual threads with a single property: `spring.threads.virtual.enabled=true` (requires Java 21+).
Traditional thread pool parameters (core/max pool size, queue capacity) become irrelevant when virtual threads are enabled.
Use `Semaphore` to limit concurrency to external services, not to limit the number of virtual threads.
Always set timeouts on I/O operations (database, HTTP, RPC) to prevent unbounded task accumulation.
Use `jcmd` for thread dumps and JFR for monitoring virtual thread behavior in production.
Virtual threads are not a replacement for WebFlux; they target different programming models and use cases.
Conclusions

The most significant shift is not technical but architectural: virtual threads let teams keep their synchronous, blocking code and still achieve high concurrency. This removes the primary motivation for adopting reactive frameworks in many traditional Spring Boot projects.

The pinning issue with `synchronized` on JDK 21-23 is a real production risk that teams must audit for. The fact that JDK 24 fixes it means many organizations will skip JDK 21-23 for virtual thread adoption and wait for 24.

The advice to use `Semaphore` for downstream rate limiting is a critical pattern. Virtual threads make it easy to create massive concurrency, but downstream services cannot handle it — the bottleneck just shifts from threads to external capacity.

The guide's emphasis on connection pools and timeouts reveals a common misunderstanding: virtual threads do not magically make resources infinite. They only make the waiting cheaper.

The comparison with WebFlux is honest and useful. Virtual threads are not a universal upgrade; they are a better fit for the majority of existing Java applications that use blocking I/O.

The recommendation to avoid `ThreadLocal` for caching expensive objects is a subtle but important warning. Developers used to pooling platform threads may inadvertently create memory pressure with virtual threads.

The fact that traditional thread pool parameters become irrelevant is a major operational change. Teams monitoring thread pool metrics will need to shift their observability strategy entirely.

Concepts & terms
Virtual Thread (Project Loom)
A lightweight thread managed by the JVM, not directly mapped 1:1 to an OS thread. Virtual threads can be created in large numbers and are automatically unmounted from their carrier thread during blocking I/O, allowing the carrier thread to serve other virtual threads.
Carrier Thread
A platform thread that temporarily hosts the execution of a virtual thread. When a virtual thread blocks on I/O, the JVM can unmount it and reuse the carrier thread for other virtual threads.
Thread Pinning
A situation where a virtual thread cannot be unmounted from its carrier thread, typically because it is inside a `synchronized` block or native method. Pinning reduces the effectiveness of virtual threads by blocking the carrier thread.
JEP 491
A JDK enhancement proposal delivered in JDK 24 that improves the JVM's monitor implementation so that virtual threads inside `synchronized` blocks can also be unmounted, largely eliminating the pinning issue.
Executors.newVirtualThreadPerTaskExecutor()
The recommended executor service for virtual threads. It creates a new virtual thread for each submitted task, avoiding the need for a fixed-size thread pool.
Semaphore (for rate limiting)
A concurrency utility used to limit the number of threads accessing a specific resource. In the context of virtual threads, it is used to cap concurrent calls to downstream services, not to limit the total number of virtual threads.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗