Interview Prep Zoneby CuriouserLabs

Question 1 of 5 · staff level

Explain virtual threads. How do they work, and when do they replace reactive programming or thread pools?

🎤 Say this first

Virtual threads (Java 21, JEP 444) are JVM-managed threads so cheap you can have millions: when one blocks on I/O, the JDK unmounts it from its carrier (OS) thread — stack moved to heap — and the carrier runs another virtual thread. You keep simple thread-per-request, blocking-style code but get event-loop-class scalability for I/O-bound work. They don't speed up CPU-bound work, and they're not for pooling — create one per task.

The full picture

🎬 Platform threads vs virtual threads

1 / 5
Request 1OS thread 1runningRequest 2OS thread 2runningRequest 3OS thread 3runningRequest 4OS thread 4running1 request = 1 OS thread. Blocking I/O holds a ~1 MB kernel resource hostage.

Platform-thread model: each request owns one OS thread (~1 MB stack, costly to create). 4 requests = 4 OS threads.

Platform model vs virtual threads mounting/unmounting on carriers.
  • Mount/unmount: parking on I/O, locks (park), sleep → unmount. The JDK's entire blocking I/O stack was retrofitted to be virtual-thread-aware.
  • Pinning: native calls / JNI, and (before Java 24 / JEP 491) blocking inside a synchronized block, pin the VT to its carrier — under load this could starve carriers; monitor with -Djdk.tracePinnedThreads.
  • Don't pool them: a pool exists to ration expensive things; VTs are cheap. Replace newFixedThreadPool(200) with newVirtualThreadPerTaskExecutor() — and express concurrency limits with a Semaphore instead of pool size.
  • ThreadLocal caution: millions of threads × fat ThreadLocals = memory surprise; scoped values (JEP 506, final in 25) are the successor for context propagation.

Versus reactive: virtual threads deliver most of the scalability that pushed teams to WebFlux/RxJava for I/O concurrency, without the viral API, the debugging opacity, or the two-colored function problem. Reactive still earns its place for streaming semantics — backpressure over unbounded event flows, windowing, composition of event sources. For 'lots of concurrent blocking calls', the pendulum has swung back to simple code on VTs (structured concurrency, JEP 505, gives it supervision trees).

🔄 Likely follow-up questions

  • What is structured concurrency and what failure-handling problem does it fix?
  • How do you detect and fix carrier pinning in production?
  • Spring Boot: what does spring.threads.virtual.enabled=true actually change?