Virtual Threads

One Property

Tomcat normally uses a pool of 200 platform threads. Each request holds a thread even while it waits for the database or an HTTP call. Java 21 virtual threads are cheap (thousands of them cost a few MB), so blocking is no longer expensive. Spring Boot 3.2+ switches Tomcat, @Async and scheduling to virtual threads with one property:

Example

application.yml

spring:
  threads:
    virtual:
      enabled: true      # needs Java 21+

Example

Log lines now show virtual threads:

curl -s localhost:8080/api/products/3

Example output

INFO [shopease] [tomcat-handler-17] c.s.ProductController : GET /api/products/3
-- Thread.currentThread() = VirtualThread[#93,tomcat-handler-17]/runnable@ForkJoinPool-1-worker-3

10,000 Blocking Tasks

Example

Each task "waits" 100 ms (like a DB call). 10,000 virtual threads finish in a fraction of that time; a pool of 200 platform threads would need about 5 seconds:

import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        AtomicInteger done = new AtomicInteger();
        long start = System.nanoTime();
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            IntStream.range(0, 10_000).forEach(i -> executor.submit(() -> {
                Thread.sleep(100);                  // blocking I/O: the virtual thread unmounts, carrier is reused
                done.incrementAndGet();
                return i;
            }));
        }                                           // close() waits for all tasks
        long ms = (System.nanoTime() - start) / 1_000_000;
        System.out.println("completed: " + done.get());
        System.out.println("under 3 seconds: " + (ms < 3000));
        System.out.println("200 platform threads would need about " + (10_000 / 200) * 100 + " ms");
    }
}

Output

completed: 10000
under 3 seconds: true
200 platform threads would need about 5000 ms

When It Helps and Watch-Outs

HelpsDoes not help
many requests waiting on DB / HTTP / filesCPU-heavy work (image resizing, PDF rendering)
replacing reactive code just for scalabilitywhen the database pool is the real limit
  • The DB pool is still 20 connections: 10,000 virtual threads will queue for them. Keep connection-timeout short.
  • Avoid long synchronized blocks around I/O on older JDKs (pinning; fixed in Java 24).
  • Do not pool virtual threads, and avoid big ThreadLocal caches.