跪拜 Guibai
← All articles
Backend

Swapping SpringBoot's Thread Pool to Virtual Threads Delivers a 200x Speedup on IO Workloads

By 神奇小汤圆 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Most Java web services are IO-bound, and the default Tomcat thread pool caps concurrency at a few hundred threads, turning latency spikes into a hard ceiling under load. Virtual threads remove that ceiling with a one-line executor change, making the JVM competitive with Go's goroutine model without rewriting code.

Summary

A straightforward SpringBoot configuration swap — wiring `Executors.newVirtualThreadPerTaskExecutor()` into both the async task executor and Tomcat's protocol handler — produces dramatic throughput gains on IO-bound work. In a benchmark that fires 100,000 `@Async` calls each sleeping 50ms, the default platform-thread pool needed 678 seconds; virtual threads finished in 3.9 seconds, roughly a 200x improvement.

Under an HTTP load test with 500 concurrent users hitting a 50ms sleep endpoint 10,000 times, the standard Tomcat thread pool pushed median latency past 150ms as the 200-thread ceiling throttled requests. Virtual threads kept even the maximum response time under 100ms by multiplexing many virtual threads onto fewer OS threads, eliminating the queuing bottleneck.

The caveat is workload shape: these gains apply to IO-intensive applications — web servers waiting on databases, caches, or downstream HTTP calls. CPU-bound tasks see little benefit because they don't block frequently enough for virtual-thread scheduling to matter.

Takeaways
100,000 async tasks each sleeping 50ms took 678 seconds with the default platform-thread pool and 3.9 seconds with virtual threads — roughly 200x faster.
Under 500 concurrent HTTP requests to a 50ms endpoint, virtual threads kept all response times below 100ms while the standard pool pushed median latency past 150ms.
Virtual threads are scheduled by the JVM onto a small number of OS threads, so one OS thread can drive tens of thousands of virtual threads.
SpringBoot 3.1.2 and Java 20+ need only a single configuration class that provides a `VirtualThreadPerTaskExecutor` to both the async executor and Tomcat.
The performance gain is specific to IO-intensive workloads; CPU-bound tasks that rarely block won't see the same improvement.
Conclusions

The 200x gap is less about virtual threads being fast and more about platform threads being catastrophically slow under high IO concurrency — 200 threads all sleeping leaves the CPU idle while work queues up.

Tomcat's default 200-thread ceiling is a relic of the one-thread-per-request model; virtual threads make that model viable at much higher concurrency without switching to reactive or async Servlet APIs.

Virtual threads don't require application rewrites — the same `Thread.sleep()` and `@Async` code runs unchanged, which lowers the migration cost compared to adopting reactive frameworks like WebFlux.

Concepts & terms
Virtual Threads
Lightweight threads introduced in Java 19 (final in Java 21) that are managed by the JVM rather than the OS. Many virtual threads map onto a small number of OS threads, allowing millions of concurrent threads with low overhead.
IO-Bound vs CPU-Bound
IO-bound tasks spend most of their time waiting for input/output operations (network, disk, database), so they benefit from high concurrency. CPU-bound tasks are limited by processor speed and gain little from extra threads.
Tomcat Protocol Handler
The component in embedded Tomcat that manages incoming HTTP connections and dispatches them to worker threads. Replacing its executor with a virtual-thread executor changes how requests are scheduled.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗