Spring Boot 3.2+ Virtual Threads with Embedded Tomcat
- Last updated
- Prerequisites:
- Spring Boot MVC basics
- Understanding of threads and Project Loom virtual threads
- spring
- spring-boot
- virtual-threads
- tomcat
- loom
- threading
Read at your depth
The practical view
Spring Boot 3.2+ supports virtual threads for the web stack: set spring.threads.virtual.enabled=true and the embedded Tomcat uses virtual threads per request instead of a fixed thread pool. Requires JDK 21+. This lets the app handle far more concurrent requests without tuning max-threads (Tomcat's default 200 platform threads becomes effectively unlimited virtual threads). Virtual threads are cheap — millions can exist. Blocking code (JDBC, RestTemplate) no longer wastes a platform thread; the carrier thread is freed while the virtual thread blocks. For Spring MVC, this is the main benefit: thread-per-request with virtual threads.
The same idea in other frameworks
N/A — server-side Spring/Tomcat concern.N/A — Spring concern.N/A — Spring concern.N/A — Spring concern.Legacy vs modern
Fixed Tomcat thread pool vs virtual threads
A fixed pool (default 200) caps concurrency and forces tuning; virtual threads lift the cap so blocking handlers no longer exhaust the pool.
server.tomcat.threads.max=200 # ❌ OLD: bounded pool — saturation under blocking loadspring.threads.virtual.enabled=true # ✅ NEW: virtual-thread-per-requestInterview gotchas
Context
This is the key Loom limitation — interviewers want the pinning mechanism and its practical triggers (synchronized, native calls), because Spring code often hits it.
The mechanical answer
Pinning happens when a virtual thread blocks while holding a monitor (inside a synchronized block) or when it calls a native method that blocks. In those cases, the JVM cannot unmount the virtual thread from its carrier thread (the lock is associated with the OS thread), so the carrier thread is blocked along with the virtual thread — defeating the whole point of virtual threads for that code path. If many virtual threads pin, the small carrier pool is exhausted and throughput drops back toward platform-thread behavior. In Spring apps, pinning triggers include legacy synchronized blocks in shared code, synchronized Collections, some JDBC drivers, and native crypto calls. The mitigation: prefer ReentrantLock (JVM-supported, non-pinning) over synchronized where practical, and check for pinning with -Djdk.tracePinnedThreads=full.
Trap
The naive answer is 'virtual threads eliminate all blocking costs'. Pinning is the exception that keeps a carrier thread occupied. Another trap: assuming JDBC automatically benefits — the DB call itself blocks the virtual thread fine, but a synchronized driver or connection pool can pin. Candidates who describe the monitor-based pinning and the trace flag show real Loom understanding.
Context
This tests the honest limits — interviewers want to know where virtual threads don't apply (CPU-bound work, thread-locals, native code) and what remains important.
The mechanical answer
Virtual threads help I/O-bound, blocking-heavy code — the typical Spring MVC request path. They do NOT speed up CPU-bound work: CPU-bound tasks still occupy a carrier thread for their full duration, so you need parallelism matching cores, not more virtual threads (a virtual thread per CPU task just adds scheduling overhead). ThreadLocal-heavy code is a concern: each virtual thread carries its ThreadLocals (Spring and libraries use them, e.g. transaction synchronization, security context), and with millions of virtual threads that's memory and copy cost; ScopedValue is the future replacement. Native code and synchronized blocks can pin (see the pinning gotcha). Also, exactly-once concurrency limits (e.g. a max 5 concurrent calls to a legacy system) still need a semaphore/bulkhead — virtual threads don't remove the need for explicit concurrency bounds.
Trap
The naive answer is 'virtual threads fix all concurrency problems'. They don't bound CPU parallelism or remove the need for semaphores/bulkheads around external rate limits. Another trap: assuming ThreadLocal cost is negligible at scale — per-virtual-thread ThreadLocals multiply with request count. Candidates who enumerate the CPU/pinning/ThreadLocal caveats show real engineering judgment.