skip to content

For which kinds of workloads do virtual threads help, and where do they give no benefit?

level: middleimportance: must knowfreq 65%

answer

  1. I/O-bound = mostly waiting -> VTs shine
  2. CPU-bound = mostly computing -> bounded by cores, no VT benefit
  3. Blocking I/O unmounts the VT, freeing the carrier
  4. VTs = reactive scalability, imperative simplicity
  5. CPU work: size to availableProcessors(), use a platform/fork-join pool

basics

~20 s

Virtual 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 s

Virtual 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

for a junior

Can state that virtual threads help I/O-bound (waiting) work and not CPU-bound (computing) work.

for a middle

Explains the unmount-on-block mechanism for I/O and that CPU throughput is capped by cores, so virtual threads add no parallelism there.

for a senior

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.

for a principal

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

context