For which kinds of workloads do virtual threads help, and where do they give no benefit?
answer
- I/O-bound = mostly waiting -> VTs shine
- CPU-bound = mostly computing -> bounded by cores, no VT benefit
- Blocking I/O unmounts the VT, freeing the carrier
- VTs = reactive scalability, imperative simplicity
- CPU work: size to availableProcessors(), use a platform/fork-join pool
basics
~20 sVirtual threads shine for I/O-bound work — tasks that spend most of their time waiting on the network, disk, or other services. For CPU-bound work they give no benefit, because the real limit is the number of CPU cores, not the number of threads.
solid answer
~50 sVirtual threads win when tasks are I/O-bound — they spend most of their time blocked waiting on the network, a database, or the filesystem. When a virtual thread blocks on I/O, the JVM unmounts it from its carrier thread, so a handful of OS threads can drive millions of concurrent waiting tasks. That lets you write simple blocking, thread-per-request code that scales like async/reactive code without the callback complexity. For CPU-bound work — heavy computation that never blocks — virtual threads give no speedup: throughput is bounded by the number of physical cores, and you should size concurrency to roughly the core count (e.g. a fixed platform-thread pool or a work-stealing pool). Spawning a million virtual threads for CPU work just adds scheduling overhead with no parallelism gain, because they all still compete for the same cores.
go deeper
Can state that virtual threads help I/O-bound (waiting) work and not CPU-bound (computing) work.
Explains the unmount-on-block mechanism for I/O and that CPU throughput is capped by cores, so virtual threads add no parallelism there.
Frames virtual threads as enabling thread-per-request simplicity at reactive scale for I/O, sizes CPU work to core count with a fork-join/platform pool, and is aware of pinning caveats.
Reasons about mixed workloads and where to place CPU work vs I/O work in the same service, weighs migrating from reactive stacks, and accounts for pinning/native-call edge cases and JDK-version behavior when advising adoption.
## Two kinds of work Every task spends its time in one of two ways: - **I/O-bound**: most of the elapsed time is spent *waiting* — for a network response, a database query, a disk read. The CPU is idle during the wait. - **CPU-bound**: most of the time is spent *computing* — hashing, encoding, number-crunching. The CPU is busy the whole time. The right concurrency strategy depends on which you have. ## How virtual threads handle blocking A **virtual thread** runs on top of a **carrier** (a platform/OS thread). The JVM's magic is this: when a virtual thread executes a *blocking* operation (network read, `sleep`, most JDK blocking calls), the JVM **unmounts** it from its carrier and parks it as a cheap heap object. The carrier is now free to mount and run a *different* virtual thread. When the I/O completes, the parked virtual thread is **remounted** on some carrier and continues. So a few carriers (by default about as many as you have cores) can keep millions of *waiting* virtual threads alive. This is why virtual threads are transformative for I/O. ## Why I/O-bound work benefits Before virtual threads, handling 10,000 concurrent slow network requests with one platform thread each was impossible — 10,000 OS threads exhaust memory and the scheduler. The workaround was **asynchronous / reactive** programming (callbacks, `CompletableFuture`, reactive streams), which is fast but hard to read, debug, and profile (stack traces become meaningless). Virtual threads let you go back to the simple **thread-per-request**, straight-line blocking style — `var data = http.get(url); var row = db.query(...);` — and still scale to millions of concurrent requests, because the blocking calls unmount instead of pinning an OS thread. You get reactive-level scalability with imperative-level simplicity. ## Why CPU-bound work does NOT benefit For pure computation there is no blocking, so nothing ever unmounts. The work can only proceed as fast as the **physical cores** can execute it. If you have 8 cores, at most 8 CPU-bound tasks make progress at once — whether you spawn 8 threads or 8 million. Extra threads beyond the core count don't add parallelism; they only add **scheduling and context-switch overhead** and memory churn. So for CPU-bound work the guideline is unchanged from the classic advice: size your concurrency to roughly the number of cores (e.g. `Runtime.getRuntime().availableProcessors()`), typically a **fixed platform-thread pool** or a **work-stealing / fork-join pool**. Virtual threads neither help nor (much) hurt here, but they add no value, so there is no reason to reach for them. ## A nuance: pinning There is one I/O case where virtual threads historically didn't unmount: blocking inside a `synchronized` block ("**pinning**") tied the virtual thread to its carrier. This was a known limitation in Java 21–23; JEP 491 (Java 24) largely eliminated `synchronized` pinning. Native-method/foreign calls can still pin. The mitigation when pinning matters is to replace `synchronized` with a `ReentrantLock` around the blocking call. ## Summary I/O-bound: virtual threads are ideal — simple blocking code that scales like async. CPU-bound: no benefit — parallelism is capped by cores, so use a small pool sized to the core count.
- What is the maximum useful parallelism for a CPU-bound task on an 8-core machine?About 8 — equal to the number of physical cores. Beyond that, extra threads (virtual or platform) only add scheduling overhead and contention, since at most 8 instructions streams can truly run at once.
- Why are virtual threads described as giving 'reactive scalability with imperative simplicity' for I/O?You can write simple, readable straight-line blocking code (one thread per request) instead of callbacks/reactive streams, yet still serve millions of concurrent I/O-bound requests, because each blocking call unmounts the virtual thread instead of holding an OS thread.
saying these in an interview costs you the question
- Claiming virtual threads make CPU-bound code faster — core count is the ceiling
- Thinking a million virtual threads = a million cores of parallelism
- Believing virtual threads replace async/reactive for raw throughput rather than for simplicity at scale on I/O
- Assuming any blocking call unmounts — ignoring that pinning (e.g. native calls, historically synchronized) can keep a VT on its carrier