The full picture
🎬 Platform threads vs virtual threads
1 / 5Platform-thread model: each request owns one OS thread (~1 MB stack, costly to create). 4 requests = 4 OS threads.
- 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
synchronizedblock, 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)withnewVirtualThreadPerTaskExecutor()— and express concurrency limits with aSemaphoreinstead 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).