Java & Spring Boot
Java & Spring Boot/senior/freq 4/5

Virtual Threads (Project Loom)

Virtual threads (JDK 21+) make blocking I/O cheap again. Treat them as the default for request handling; keep platform threads for CPU-bound work.

javaconcurrencyloom

Deep dive

What changed

Before Loom, scaling I/O-heavy services meant reactive code (WebFlux, RxJava) — non-trivial to debug. Virtual threads let you keep imperative, blocking code while the JVM multiplexes thousands of them onto a small carrier pool.

When they shine

HTTP request handlers that mostly wait on DB, downstream HTTP, or message brokers. One virtual thread per request scales to hundreds of thousands.

When they don't

  • CPU-bound work: virtual threads give no benefit — use ForkJoinPool or platform threads.
  • Pinning: synchronized blocks pin the carrier thread. Use ReentrantLock instead, or wait for JDK 24+ which removes most pinning.
  • ThreadLocal abuse: virtual threads are cheap, so a ThreadLocal per request now means millions of objects. Prefer ScopedValue.

Real-world example

From production

A gateway service handling 30k req/s on WebFlux was hard to debug — stack traces were useless. Migrated to Spring Boot 3.2 + virtual threads (spring.threads.virtual.enabled=true), kept the imperative code, and matched throughput with simpler operations. Stack traces became readable; on-call burden dropped.

Interview questions

2 senior-level
Q1When would you NOT use virtual threads?

CPU-bound work (compression, encryption, heavy serialization) — they give no benefit and add overhead. Also avoid in code that uses synchronized heavily on hot paths until JDK 24, due to carrier-thread pinning.

Q2Virtual threads vs reactive (WebFlux)?

Both target I/O scalability. Reactive is harder to write, debug, and integrate with blocking libraries, but gives backpressure primitives. Virtual threads keep imperative code and tools. For most CRUD-ish services, virtual threads now win on simplicity.

Common mistakes

  • Pooling virtual threads — they're meant to be created and discarded.

  • Sharing a fixed-size DB connection pool with thousands of virtual threads (you'll just queue on the pool).

  • Using synchronized on hot paths and getting silent carrier-thread pinning.

Trade-offs

  • Virtual threads don't add backpressure — your DB pool or downstream service becomes the bottleneck.

  • Reactive remains valuable when you need explicit backpressure or stream composition.

Related