Virtual threads arrived and half the internet declared blocking I/O solved forever. The other half assumed their slow code would magically speed up. Both are wrong. Virtual threads are excellent, but they do something narrower than the hype suggests.
What They Actually Do
A platform thread maps to an OS thread — heavy, limited to a few thousand. A virtual thread is managed by the JVM and costs almost nothing, so you can have millions. When a virtual thread blocks on I/O, the JVM parks it and reuses the underlying OS thread for other work.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0; i < 100_000; i++) { executor.submit(() -> { var response = httpClient.send(request, ofString()); // blocks cheaply return response.body(); }); } }
A hundred thousand concurrent blocking calls, no thread pool tuning. That is the win.
The Misunderstanding
Virtual threads make blocking cheap, not fast. If a database call takes 200ms, it still takes 200ms on a virtual thread. What changes is that you can have vastly more of those calls in flight without exhausting threads. Throughput under concurrency goes up; the latency of any single call does not.
If your code is slow because of CPU work — parsing, serialization, a bad algorithm — virtual threads do nothing. They help exactly one thing: workloads dominated by waiting on I/O.
The Real Gotchas
- ▸Pinning. A virtual thread that blocks inside a
synchronizedblock cannot be unmounted, so it pins the carrier thread and you lose the benefit. PreferReentrantLock. - ▸Pooling them is pointless. Virtual threads are cheap to create. Do not put them in a fixed pool — create one per task. Pooling reintroduces the limit you were trying to escape.
- ▸Thread-locals still cost memory. A million virtual threads each holding a fat
ThreadLocalis a million copies.
Where I Land
Virtual threads are a genuine upgrade for I/O-bound server code, and they let you keep writing simple, straight-line blocking code instead of tangled reactive chains. But they are not a performance button. They raise your concurrency ceiling; they do not make any individual operation quicker, and they do nothing for CPU-bound work.
Adopt them for high-concurrency I/O. Do not expect them to fix code that was slow for other reasons. Know the difference, and you will actually get the benefit.