When virtual threads help and when they don't (CPU-bound… — Cracked Java
// Spring Framework & Spring Boot · Async, Scheduling, Virtual Threads
SeniorTheoryTrick

When virtual threads help and when they don't (CPU-bound vs IO-bound, pinning).

Virtual threads help when work is IO-bound and blocking — waiting on a database, an HTTP call, a queue — and do essentially nothing for CPU-bound work, because the win is "a blocked thread costs nothing," and CPU work is never blocked, it's busy. Knowing exactly which side a workload falls on is the senior judgment this question tests.

Where they shine: IO-bound / blocking

A request that spends most of its time waiting on downstream IO is the perfect case. With platform threads you'd exhaust a bounded pool under load; with virtual threads, a blocked thread unmounts from its carrier and frees the OS thread for others. So thousands of concurrent requests each making blocking JDBC/HTTP calls scale on a handful of carrier threads.

// IO-bound: 95% of the time is spent waiting -> virtual threads win big
Order o = jdbc.queryForObject(...);
Shipment s = restClient.get()...body(Shipment.class);

Where they don't: CPU-bound

If a task is crunching numbers, encoding video, or running a tight loop, it never yields by blocking — it just occupies a carrier thread continuously. You can't run more CPU work than you have cores, so spawning a million virtual threads for CPU work only adds scheduling overhead. For CPU-bound work, use a bounded pool sized to the cores (ThreadPoolTaskExecutor with core/max ≈ available processors), not virtual threads.

WorkloadVirtual threads?
DB queries, HTTP/REST calls, blocking IOYes — large win
Message consumers waiting on a brokerYes
Heavy computation, parsing, crypto, image/videoNo — use a fixed pool

The pinning caveat

A virtual thread normally unmounts when it blocks. But if it blocks inside a synchronized block/method (or a native frame), it pins the carrier — the carrier OS thread is held and can't run other virtual threads, partially defeating the point. Pre-JDK 24 fix: replace synchronized around IO with a ReentrantLock. JDK 24 largely resolves pinning for synchronized, so it stops being a practical concern there — but native-method and a few other pins remain.

// pins the carrier while blocked (pre-JDK 24):
synchronized (lock) { restClient.get()...retrieve(); }
// prefer:
lock.lock();
try { restClient.get()...retrieve(); } finally { lock.unlock(); }

Mark your status