Virtual threads in a Spring Boot service

Spring Boot · lesson 33 of 39 · 5 min read

When they help, when they pin, and what they do not fix.

Open this lesson in the learning hub

Key points

  • On Java 21, spring.threads.virtual.enabled=true makes Tomcat serve each request on a virtual thread. A blocking call then unmounts the carrier thread instead of holding it.
  • This is why it matters: the old ceiling was a few hundred platform threads, each costing about a megabyte of stack. A blocking service was limited by thread count long before it was limited by CPU.
  • It gives you the scalability of reactive code without rewriting anything into a reactive chain - the programming model stays blocking and debuggable, with real stack traces.
  • It does not make anything faster. One request takes exactly as long; you can simply have far more of them in flight.
  • It does not remove the real bottlenecks either. A connection pool of ten still admits ten concurrent queries, and ten thousand virtual threads queuing on it just moves the wait.
  • Before JDK 24, a virtual thread inside a synchronized block pinned its carrier thread and could not unmount. Library code holding a monitor across I/O was the usual culprit; a ReentrantLock does not pin.

Example

# Boot 3.2 or newer, on Java 21 or newer.
spring:
  threads:
    virtual:
      enabled: true

# Pinning was the trap before JDK 24. Detect it rather than guessing:
#   java -Djdk.tracePinnedThreads=full -jar app.jar

# Sizing still matters. Virtual threads remove the THREAD limit, not the
# resource limit - the pool below is the real concurrency ceiling for queries.
  datasource:
    hikari:
      maximum-pool-size: 20

---
# The trade-off, stated plainly:
#
#   Platform threads   200 threads x 1 MB stack   -> ~200 concurrent blocking requests
#   Virtual threads    heap-allocated stacks      -> tens of thousands, same code
#
# What does NOT change:
#   - per-request latency
#   - database pool size
#   - downstream rate limits
#   - the need for timeouts on every remote call

Virtual threads raise the concurrency ceiling of blocking code; they do not make a request faster or a connection pool bigger.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Spring Boot course, and every lesson in it is listed on the Spring Boot contents page.