Where do Fork/Join tasks run, and what is the danger of performing blocking I/O or otherwise blocking inside a RecursiveTask/RecursiveAction's compute()?
answer
- Runs on ForkJoinPool; common pool ≈ cores − 1
- Common pool is shared JVM-wide (streams, CompletableFuture)
- Blocking parks a scarce worker -> stall/deadlock
- Keep compute() CPU-bound; do I/O elsewhere
- ManagedBlocker spawns a compensating thread
basics
~20 sFork/Join tasks run on a ForkJoinPool, which by default has about one worker thread per CPU core (the common pool). If your compute() blocks on I/O or a lock, that worker can't do other work, so a few blocked tasks can stall the whole pool. Fork/Join is meant for CPU-bound, splittable work — keep blocking out of compute().
solid answer
~60 sRecursiveTask/RecursiveAction run inside a ForkJoinPool — usually the shared common pool, sized to roughly the number of cores minus one by default. The framework assumes tasks are CPU-bound and short: a worker computes a leaf, then helps run other queued tasks while joining. If a task blocks (network/file I/O, a lock, Thread.sleep, an external future), that worker thread is parked and unavailable, so the pool loses a core's worth of throughput. Block enough workers and the whole common pool — shared with parallel streams and CompletableFuture across the JVM — grinds to a halt or deadlocks. Mitigations: don't do blocking work in Fork/Join at all (use a dedicated ExecutorService for I/O); if you must block within Fork/Join, wrap it in ForkJoinPool.ManagedBlocker so the pool can spawn a compensating thread to maintain parallelism; and avoid using the common pool for anything that might block — create your own ForkJoinPool. The framework also limits how a join interacts with blocking, which is why join()-style waiting is fine but arbitrary blocking is not.
go deeper
Knows Fork/Join tasks run on a thread pool (ForkJoinPool) rather than threads you create directly.
Knows the common pool is roughly core-sized and that blocking in compute() reduces parallelism.
Explains common-pool sharing, the stall/deadlock risk from blocking scarce workers, ManagedBlocker, and the rule to do I/O on a separate executor / custom pool.
Designs around it: chooses Fork/Join only for CPU-bound work, isolates blocking on dedicated/virtual-thread executors, reasons about common-pool poisoning in libraries, and weighs ManagedBlocker vs restructuring the workload.
## Where the tasks run When you call `task.invoke()`, `pool.invoke(task)`, or use a parallel stream, the work executes on a **`ForkJoinPool`** — a thread pool of *worker threads*, each owning a local **deque** of tasks and stealing from others when idle. There are two ways a pool is involved: - **The common pool** (`ForkJoinPool.commonPool()`): a single JVM-wide shared pool used by parallel streams, `CompletableFuture` default async methods, and `RecursiveTask.invoke()` when you don't supply a pool. Its default parallelism is **`Runtime.getRuntime().availableProcessors() - 1`** (one less than cores, because the submitting thread also helps). - **A custom pool** you construct with `new ForkJoinPool(parallelism)` and submit to explicitly. The crucial design assumption: **workers are kept busy with CPU-bound, non-blocking, splittable work.** A worker finishes a leaf, then picks up / steals more tasks. `join()` is special — while a worker waits on `join()`, it can *execute other ready tasks* instead of truly parking, which is what makes the model scale. ## Why blocking is dangerous If your `compute()` does something that **blocks the thread** — network call, file/database I/O, acquiring a contended lock, `Thread.sleep`, waiting on an unrelated `Future`/`CountDownLatch` — that worker thread is **parked and removed from useful service**. The pool has only ~(cores − 1) workers. Consequences escalate: 1. **Lost throughput:** each blocked worker = one fewer core doing work. Block half the workers and you've halved parallelism. 2. **Starvation / stall:** because the common pool is **shared JVM-wide**, blocking it in one place starves *everything else* using it — every parallel stream and default `CompletableFuture` in the process. A library doing blocking I/O on the common pool can mysteriously freeze unrelated code. 3. **Deadlock:** if blocked tasks are waiting on results that can only be produced by other tasks that now can't get a worker, the pool deadlocks. With limited parallelism this is easy to trigger. ## Mitigations - **Don't block in Fork/Join.** Keep `compute()` CPU-bound. Do I/O on a **separate, appropriately sized `ExecutorService`** (e.g. a cached or fixed pool, or virtual-thread executor on modern Java) designed to tolerate many blocked threads. - **Never run blocking work on the common pool.** If you need Fork/Join-style parallelism around something that might block, create a **dedicated `ForkJoinPool`** so you don't poison the shared one. - **`ManagedBlocker`:** if you genuinely must block *inside* a Fork/Join computation, wrap the blocking call in **`ForkJoinPool.ManagedBlocker`** and call `ForkJoinPool.managedBlock(...)`. This signals the pool that the worker is about to block so it can **spawn a compensating thread**, preserving the target parallelism. It's the supported escape hatch but adds complexity. - **Prefer virtual threads / dedicated executors for I/O-heavy fan-out** on modern JDKs — Fork/Join is the wrong tool for blocking workloads. ## Why join() itself is fine A `join()` on a *subtask of the same computation* is cooperative: the waiting worker helps run other ready tasks rather than dead-parking, and the framework understands this dependency. That's categorically different from blocking on an *external* event the pool knows nothing about. ## Summary - Tasks run on a `ForkJoinPool`, usually the JVM-wide common pool (~cores − 1 workers). - Blocking I/O/locks in `compute()` parks scarce workers → lost throughput, cross-feature starvation, and potential deadlock. - Keep Fork/Join CPU-bound; do blocking elsewhere; use a custom pool (never the common pool) for blocking; and use `ManagedBlocker` if you truly must block inside it.
- What is ForkJoinPool.ManagedBlocker for?It lets a Fork/Join worker block in a way the pool understands: before blocking, the pool can start a compensating worker thread so overall parallelism is maintained. It's the supported way to block inside a Fork/Join computation without starving the pool.
- Why is using the common pool for blocking work especially bad?The common pool is shared by the whole JVM — parallel streams and default CompletableFuture async tasks all use it. Blocking its few workers starves unrelated code across the application, not just your own task.
saying these in an interview costs you the question
- Doing blocking network/DB I/O directly inside compute()
- Running blocking work on the common pool (poisons all parallel streams/CompletableFuture)
- Assuming the pool grows threads automatically to cover blocked workers (it doesn't, unless ManagedBlocker)
- Confusing join() (cooperative, fine) with arbitrary external blocking (dangerous)
- Thinking Fork/Join is a general-purpose executor for any workload