What they're testing
Whether you've kept up with current Java, and whether you distinguish I/O-bound from CPU-bound work.
The short answer~30 seconds
Virtual threads (JEP 444, final in Java 21) are JVM-managed threads cheap enough to create by the million. When one blocks on I/O the JVM unmounts it from its carrier OS thread instead of holding it — so the readable thread-per-request model scales to volumes that previously demanded reactive code. They do NOTHING for CPU-bound work: you still have the cores you have.
The long answer
The largest shift in thinking is that thread pools mostly stop being necessary. Pools existed because OS threads were expensive — roughly a megabyte of stack each plus a kernel context switch. Virtual threads are cheap, so creating one per task is right; you cap concurrency with a semaphore where the limit actually belongs (say, connections to one downstream service) rather than with a pool size.
The trap worth naming is pinning. When a virtual thread blocks inside a synchronized block, on Java 21 it's pinned to its carrier and can't unmount — so you lose exactly the benefit just described, and with enough pinned threads the application stalls. The remedy is replacing synchronized with ReentrantLock where I/O blocking happens inside. JEP 491 (Java 24) removed the limitation, so a complete answer states the version.
The second trap is ThreadLocal. It still works, but with millions of threads every value stored in one is multiplied by millions — and libraries that use ThreadLocal as a cache (very common, e.g. a wrapped SimpleDateFormat) suddenly become a memory problem. ScopedValue is the replacement designed for the new model.
Finally, and where expectations most often go wrong: virtual threads don't raise your throughput if the bottleneck is the database. You've only moved the queue from "waiting for a thread" to "waiting for a connection". Saying so shows you're looking at the whole system rather than the application tier.
// Một virtual thread cho mỗi tác vụ — không cần pool
try (var scope = Executors.newVirtualThreadPerTaskExecutor()) {
for (var id : orderIds) scope.submit(() -> process(id));
}
// Giới hạn đồng thời đặt ở chỗ có giới hạn thật
private final Semaphore downstream = new Semaphore(20);
downstream.acquire();
try { callPaymentApi(); } finally { downstream.release(); }
// Java 21: chặn trong synchronized -> pinning. Dùng khoá tường minh.
private final ReentrantLock lock = new ReentrantLock();What they'll ask next
?Is reactive (WebFlux, Reactor) still needed?
Much less so for typical CRUD services. But reactive still wins on backpressure and stream processing, which virtual threads don't provide. If your problem is "many requests blocking on I/O", virtual threads win on readability; if it's "an event stream that needs regulating", reactive is still the right tool.
?What does structured concurrency solve?
It forces child tasks to have lifetimes nested inside a scope: the parent waits for all children, one failure cancels the rest, and stack traces keep the parent-child relationship. It has stayed in preview across several releases (JEP 453 → 505), so it's worth mentioning but not yet worth depending on in production.
These lose points
- Saying virtual threads make everything faster. They make WAITING cheap, not COMPUTING fast.
- Not knowing about pinning. It's the near-certain follow-up once you raise virtual threads.