skip to content

Explain the fork()/join() pattern in Fork/Join tasks. Why is the idiomatic ordering fork-one, compute-the-other, then join — and what goes wrong if you fork both then join both, or fork-then-immediately-join?

level: middleimportance: must knowfreq 60%

answer

  1. fork = queue async; join = await + result
  2. fork-one, compute-other, join-one
  3. fork-then-immediately-join = zero parallelism
  4. fork-both-join-both = correct but wastes a fork
  5. invokeAll encapsulates the idiom; LIFO deque

basics

~20 s

fork() schedules a subtask to run asynchronously; join() waits for it and returns its result. The idiom is: fork the left subtask, run the right one yourself by calling compute() directly, then join the left. This keeps the current thread busy instead of idle, and avoids forking a task only to immediately wait on it (which gives no parallelism).

solid answer

~50 s

fork() submits a subtask to the ForkJoinPool's work queue so another worker thread can pick it up (work-stealing); join() blocks until that subtask completes and returns its result. The canonical idiom is fork one half, then call compute() directly on the other half in the current thread, then join the forked half: this way the current thread does useful work instead of sitting idle while its sibling runs elsewhere. If you instead fork() then immediately join() the same task, you get no parallelism — the current thread just waits while one subtask runs, serializing the work plus paying fork overhead. Forking both subtasks then joining both also works and is correct, but the second fork is wasted: the current thread could have executed one of them directly. Order also matters when joining: join in the reverse order you'd want for LIFO stack locality (join the most-recently-forked first), which is why invokeAll or fork-left/compute-right/join-left is preferred.

go deeper

for a junior

Knows fork() runs a subtask asynchronously and join() waits for its result.

for a middle

Writes the fork-one/compute-other/join-one idiom correctly and explains why fork-then-immediate-join gives no parallelism.

for a senior

Ties the ordering to the per-worker LIFO deque and work-stealing, knows invokeAll, and reasons about join order and why the waiting worker can still help.

for a principal

Reasons about deque locality, steal contention, and when the redundant-fork pattern still pays off (e.g. highly uneven splits or external blocking), and overhead tradeoffs versus other parallel abstractions.

## The two primitives In the Fork/Join framework a task is split into subtasks, and two methods coordinate them: - **`fork()`** — pushes this task onto the **current worker thread's local deque** (double-ended queue) and returns immediately. The task is now *eligible to run asynchronously*: either the current thread will pop it later, or an idle worker thread will **steal** it. `fork()` does **not** start a new OS thread; it just queues work for the pool. - **`join()`** — waits until the task has completed and returns its result (for a `RecursiveTask`). While waiting, the calling worker doesn't just block uselessly — it may help by executing other queued tasks (this is part of why Fork/Join scales). ## Why the idiom is fork-one / compute-other / join-one The recommended shape inside `compute()` is: ```java left.fork(); // 1. make left available to other workers R rightResult = right.compute(); // 2. do right myself, right now R leftResult = left.join(); // 3. collect left's result return combine(leftResult, rightResult); ``` Reasoning step by step: 1. **fork-one** publishes `left` so an *idle* worker can grab it in parallel. 2. **compute-the-other** keeps *this* thread productive: instead of forking `right` and then standing idle, the current thread directly runs `right`. One thread, two subtasks, zero idle time — and `right` runs without any queueing overhead. 3. **join-one** then waits for `left`. By the time we get here we've already finished `right`, so ideally `left` is also done (or nearly), minimizing the wait. This maximizes the ratio of useful work to coordination overhead, and it respects the deque's **LIFO** discipline: the current thread tends to pop the most-recently-pushed task, so forking only one and computing the other avoids fighting the work-stealing structure. ## What goes wrong with the anti-patterns **(a) fork() then immediately join() the same task:** ```java left.fork(); R leftResult = left.join(); // BAD: nothing else happening meanwhile R rightResult = right.compute(); ``` The current thread forks `left`, then *immediately blocks* waiting for it. Nothing runs in parallel — you've serialized the work and *added* the cost of queueing `left`. This is strictly worse than just calling `left.compute()`. **(b) fork both, then join both:** ```java left.fork(); right.fork(); // wasteful second fork R l = left.join(); R r = right.join(); ``` This is **correct** but suboptimal. The current thread forks both and then has nothing to do but wait, so it relies on *other* threads to run *both* halves. The current worker could have executed one of them directly (saving a queue push/pop and not depending on a steal happening). Under load it usually still works because `join()` lets the waiting worker help, but the cleaner idiom avoids the redundant fork. **(c) Wrong join order:** If you do fork both, join them in **reverse** order (`right.join()` before `left.join()` when `right` was forked last) to match the LIFO deque — the last-forked task is most likely still on the local deque and can be run immediately by this thread rather than waited on. The fork-one/compute-other idiom sidesteps this entirely. ## The convenience shortcut `invokeAll(t1, t2, ...)` forks all but the first, runs the first in the current thread, and joins the rest — encapsulating the idiom for you. It throws on the first failing subtask. Use it to avoid hand-writing the ordering. ## Key takeaways - `fork()` = queue async, `join()` = await + result. - Never `fork()` a task you'll `join()` with nothing in between. - Keep the current thread busy: compute one subtask directly. - Prefer `invokeAll` or fork-left/compute-right/join-left.

  • What does invokeAll do, and how does it relate to the idiom?
    invokeAll forks all tasks except the first, executes the first directly in the current thread, then joins the others — it bakes in the fork-one/compute-other/join idiom so you don't hand-order fork/join calls.
  • If you fork both subtasks, in what order should you join them and why?
    Join the most-recently-forked first (reverse of fork order). The local deque is LIFO, so the last-forked task is likely still on this thread's deque and can be executed immediately rather than waited on after being stolen.

saying these in an interview costs you the question

  • Claiming fork() starts a new OS thread (it only queues on a deque)
  • fork() then immediate join() of the same task — kills parallelism
  • Joining in forward order after forking both (fights the LIFO deque)
  • Thinking join() makes the worker idle — it can help run other tasks while waiting
  • Calling compute() on a forked task (double-execution) instead of join()

context