When parallelStream starves the common pool

Read the explanation
What if

A parallel stream of blocking calls fills the common pool. An unrelated quick task waits in line behind it.

0123456789buildReport(4)buildReport(1)main: waits for the streamwaitmain: stream donedonebuildReport(2)buildReport(3)buildReport(6)buildReport(5)commonPool-2: quickTask()quickhttp-1: supplyAsyncsubmitjoin(): queuedgot ok

Tap any block to see what that thread is doing.

  • Waiting on I/O
  • Waiting / parked
  • Running
Step 1/5

parallelStream splits the work over the common pool: 2 workers here, plus main itself.

1// A batch job on main:2List.of(1, 2, 3, 4, 5, 6).parallelStream()3    .forEach(id -> buildReport(id)); // slow and blocking: holds a common-pool thread4 5// Meanwhile, a request handler on http-1:6String r = CompletableFuture.supplyAsync(() -> quickTask()) // no executor: the common pool7    .join(); // waits for a free common-pool thread

Explanation

What just happened

parallelStream() doesn’t create threads of its own. It splits the work over the JVM-wide ForkJoinPool.commonPool() and lets the calling thread (main) help. Each buildReport blocked its thread, so the whole pool was busy. Then an unrelated request called supplyAsync with no executor, which uses the same pool. Its one-unit task waited until a worker ran out of report work.

Try the what-if switch

its own executor: supplyAsync(task, ioPool) runs on ioPool’s thread, so it starts at once no matter how busy the common pool is.

Why it bites

The common pool is shared by everything in the JVM: parallel streams, CompletableFuture’s *Async methods without an executor, and many libraries. It’s sized for CPU work, with one thread fewer than you have cores. A few blocking calls (database, HTTP, files) can tie it all up, and code elsewhere slows down for no visible reason.

Rules of thumb

How to spot it

Take a thread dump. If every ForkJoinPool.commonPool-worker thread is stuck in I/O, this is your problem. The thread dump reader checks for exactly this.

Common questions

Which threads does parallelStream use?

The JVM-wide ForkJoinPool.commonPool(), plus the calling thread, which helps with the work. It doesn't create threads of its own.

How many threads does the common pool have?

By default, one fewer than the number of available processors, so a 2-core container gets a single worker. With only one, CompletableFuture's async methods up to JDK 21 start a new thread per task instead. From JDK 25 they share that one worker.

How do I stop blocking work from starving the common pool?

Pass your own executor: supplyAsync(task, ioPool). Later *Async stages without an executor go back to the common pool, so pass it to each one. On JDK 21+, Executors.newVirtualThreadPerTaskExecutor() suits blocking calls.

Verified: this timeline matches a real program, checked in CI on JDK 21 and 25. How we verify. Unclear or wrong? Tell us