Virtual Threads
Java 21's Project Loom lets a blocking, readable programming model scale to huge concurrency - and Spring Boot turns it on with one property.
On this page
Back in the reactive module, you met a hard trade-off: a classic web app dedicates one OS thread per request, and OS threads are expensive, so a few thousand concurrent, I/O-bound requests can exhaust the pool - which is why reactive programming exists. Java 21 virtual threads (Project Loom) largely dissolve that trade-off, and Spring Boot turns them on with a single property.
The problem with platform threads
A traditional Java thread - a platform thread - is a thin wrapper over an OS thread. Those are heavyweight (about a megabyte of stack each) and limited, so you pool them (say 200). When a request does blocking I/O - a database call, an HTTP call - its thread sits and waits, doing nothing but occupying a slot. Under high I/O concurrency the pool fills with waiting threads and new requests queue, even though the CPU is idle:
// simple, readable - but this thread is BLOCKED and wasted while waiting on I/O
Book book = catalogClient.getBook(isbn); // ← thread parked here for, say, 80msReactive code avoids this by never blocking - but at the cost of a harder, callback/operator programming model.
Virtual threads: cheap threads, blocking code
A virtual thread is a lightweight thread scheduled by the JVM, not the OS. It costs a few hundred bytes, you can have millions, and here's the trick: when a virtual thread hits a blocking call, the JVM unmounts it from its carrier OS thread and parks it, freeing the OS thread to run another virtual thread. The blocking call looks blocking in your code, but it no longer ties up a scarce OS thread:
// the SAME simple, blocking, readable code -
// but now on a virtual thread, waiting costs almost nothing
Book book = catalogClient.getBook(isbn); // ← virtual thread unmounts while it waitsYou get reactive-level scalability for I/O-bound workloads while writing ordinary, top-to-bottom, debuggable code. In Spring Boot, that's one property:
spring:
threads:
virtual:
enabled: true # Tomcat serves each request on a virtual thread (Java 21+)Imagine a restaurant with only a few waiters (OS threads). If each waiter must stand frozen beside a table while the kitchen cooks (blocking I/O), a handful of orders paralyzes the place. Virtual threads are like letting a waiter note the order, seat the guest, and go serve others while the kitchen works, returning when the food's ready. Same simple 'one waiter per table' mental model, but a few waiters now serve a full house - because waiting no longer means standing still.
Great for I/O, not a CPU speed-up
Virtual threads help I/O-bound concurrency (lots of requests waiting on the DB/network). They do
not make CPU-bound work faster - you still have only so many cores. Also beware pinning:
holding a synchronized lock across a blocking call can pin the virtual thread to its carrier;
prefer ReentrantLock on hot paths (a JDK-version-dependent caveat that keeps improving).
Virtual threads vs. reactive - which to reach for
For the vast majority of blocking CRUD/web apps, virtual threads are now the simpler answer to 'scale my I/O concurrency': keep imperative code, flip one property. Reactive still earns its complexity for streaming, backpressure, and truly non-blocking end-to-end pipelines - but it is no longer the only way to serve high concurrency.
A BookVault endpoint calls two slow downstream services and spends most of its time waiting on I/O. Under load, the app runs out of Tomcat threads and requests queue. A colleague proposes rewriting everything reactive. Explain how enabling virtual threads addresses the same bottleneck with far less change, and when the reactive rewrite would still be justified.
What do Java 21 virtual threads enable in a Spring Boot app?
Key takeaways
- A platform thread wraps a scarce, heavyweight OS thread; blocking I/O wastes it while it waits, capping concurrency.
- A virtual thread is a lightweight JVM-scheduled thread; on a blocking call the JVM unmounts it, freeing the OS thread - so millions can exist.
- You get high I/O concurrency with simple, blocking, debuggable code - much of reactive's benefit without its complexity.
- Spring Boot enables per-request virtual threads with spring.threads.virtual.enabled=true (Java 21+).
- They help I/O-bound, not CPU-bound, work; watch for pinning from synchronized blocks across blocking calls.
- For most blocking web apps, virtual threads are now the simpler path to scale; reactive still fits streaming/backpressure.