skip to content

Fork/Join Framework

Divide-and-conquer parallelism through ForkJoinPool: splitting work recursively and letting idle workers steal it. It also underpins parallel streams and CompletableFuture, which is usually how it enters an interview.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

10

In Java's Fork/Join framework, what is the difference between RecursiveTask and RecursiveAction, and when would you use each?

level: juniorimportance: must knowfreq 55%

answer

  1. Task = returns V; Action = void
  2. compute() is the method you override
  3. Both extend ForkJoinTask
  4. Sum/count -> Task; in-place sort -> Action
  5. Question: do I combine a result? yes->Task

basics

~20 s

RecursiveTask returns a result from its compute() method; RecursiveAction does not (it returns nothing). Use RecursiveTask when you need a value back (like a sum), and RecursiveAction when the work just has a side effect (like sorting an array in place).

solid answer

~40 s

Both are abstract base classes in the Fork/Join framework that you subclass to define a parallel, recursively splittable unit of work, implementing a single compute() method. RecursiveTask<V> is generic and its compute() returns a V — you use it when each subtask produces a value you must combine, such as summing a range or counting matches. RecursiveAction's compute() returns void — you use it for work that mutates shared state in place or has a side effect, such as parallel sort or applying a transform to array slices, where there's nothing to return and combine. Functionally they're parallel: both support fork()/join() and the split-then-combine recursion. The only real difference is whether the recursion produces and merges results (Task) or not (Action). Both extend ForkJoinTask.

code

java · 32 lines
java
// RecursiveTask: returns a value
class SumTask extends RecursiveTask<Long> {
    final int[] a; final int lo, hi;
    SumTask(int[] a, int lo, int hi) { this.a = a; this.lo = lo; this.hi = hi; }
    protected Long compute() {
        if (hi - lo <= 1000) {            // base case
            long s = 0;
            for (int i = lo; i < hi; i++) s += a[i];
            return s;
        }
        int mid = (lo + hi) >>> 1;
        SumTask left = new SumTask(a, lo, mid);
        left.fork();                       // async
        long right = new SumTask(a, mid, hi).compute();
        return left.join() + right;        // combine
    }
}

// RecursiveAction: no value, mutates in place
class IncrementAction extends RecursiveAction {
    final int[] a; final int lo, hi;
    IncrementAction(int[] a, int lo, int hi) { this.a = a; this.lo = lo; this.hi = hi; }
    protected void compute() {
        if (hi - lo <= 1000) {
            for (int i = lo; i < hi; i++) a[i]++;
            return;                        // nothing to return
        }
        int mid = (lo + hi) >>> 1;
        invokeAll(new IncrementAction(a, lo, mid),
                  new IncrementAction(a, mid, hi));
    }
}

go deeper

for a junior

Knows RecursiveTask returns a value and RecursiveAction returns void, and that you override compute().

for a middle

Picks the right class for a given problem and can sketch the split/combine compute() for both, knowing both extend ForkJoinTask.

for a senior

Explains that the choice is purely about whether results are merged, ties it to work-stealing in ForkJoinPool, and notes Action-with-shared-state alternatives and their pitfalls.

for a principal

Discusses when Fork/Join is the right abstraction at all versus parallel streams or a plain executor, and the API design rationale for two classes rather than one.

## Background The **Fork/Join framework** (introduced in Java 7, in `java.util.concurrent`) is a tool for **divide-and-conquer parallelism**: you take a big problem, recursively split it into smaller subproblems, solve the small ones (possibly on different threads), then combine the answers. It is built around a special thread pool called `ForkJoinPool` that uses *work-stealing* (idle threads steal queued tasks from busy threads) to keep CPU cores busy. ## The two base classes To use the framework you subclass one of two abstract classes and implement its `compute()` method, which contains your split-and-combine logic: - **`RecursiveTask<V>`** — a task that **produces a result**. Its abstract method is `protected abstract V compute()`. The `<V>` is a generic type parameter naming the result type (e.g. `RecursiveTask<Long>` returns a `Long`). - **`RecursiveAction`** — a task that **produces no result**. Its abstract method is `protected abstract void compute()`. There is no type parameter because nothing is returned. Both extend `ForkJoinTask`, which provides the core coordination methods `fork()` (schedule this subtask to run asynchronously) and `join()` (wait for it to finish and, for a Task, get its result). ## When to use which Use **`RecursiveTask`** when each piece of work yields a **value you must merge**: - Summing the elements of a large array → each half returns a partial sum; the parent adds the two halves. - Counting how many items match a predicate. - Searching for / finding a maximum. Use **`RecursiveAction`** when the work is a **side effect with nothing to return**: - Sorting an array in place (e.g. each subtask sorts its slice; the canonical example is a parallel merge sort). - Applying an in-place transform to each slice of a large array (incrementing, normalizing, etc.). - Any "do something to a region" job where the result is the mutation itself. ## Rule of thumb Ask: *"After a subtask finishes, do I have a value I need to combine with its sibling's value?"* If **yes**, use `RecursiveTask<V>`. If **no** (the effect is the mutation), use `RecursiveAction`. ## Tiny mental model ``` compute() { if (small enough) { return solveDirectly(); // Task: return a value // or: solveDirectly(); // Action: just do it, return nothing } else { split into left/right left.fork(); // run left asynchronously rightResult = right.compute(); // do right here leftResult = left.join(); // wait for left return combine(leftResult, rightResult); // Task only } } ``` The combine step at the end is exactly what distinguishes a `RecursiveTask` from a `RecursiveAction` — the Action simply has no value to combine.

  • Which method do you override, and what is its access modifier?
    You override compute() — protected abstract V compute() for RecursiveTask, protected abstract void compute() for RecursiveAction.
  • Could you implement a sum with RecursiveAction?
    Yes, but awkwardly — you'd have to write the partial result into shared mutable state (e.g. a field or an array slot) instead of returning it, which is more error-prone. RecursiveTask<Long> is the natural fit.

saying these in an interview costs you the question

  • Saying RecursiveAction returns a result — it returns void
  • Thinking they run on plain threads rather than a ForkJoinPool
  • Believing the choice affects parallelism or performance directly (it only affects whether you return/combine a value)
  • Confusing RecursiveTask with Callable or Runnable

context

open as a page

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%

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).

open as a page

What is the ForkJoinPool common pool, who shares it, and why does blocking work on it cause problems?

level: seniorimportance: must knowfreq 62%

basics

~20 s

The common pool is a single ForkJoinPool the JVM shares process-wide. Parallel streams and CompletableFuture's default async methods all run on it. It is sized to the number of CPU cores minus one, so if you run blocking tasks on it, you starve every other feature that also uses it.

open as a page

How does ForkJoinPool's work-stealing algorithm work, and what is the role of the per-worker deques?

level: seniorimportance: must knowfreq 68%

basics

~20 s

Each worker thread has its own double-ended queue (deque) of tasks. A worker pushes and pops its own tasks from one end. When a worker runs out of work, it steals a task from the opposite end of another busy worker's deque, so idle threads stay busy and load stays balanced.

open as a page

What is the Fork/Join framework and ForkJoinPool, and what kind of workload is it designed for?

level: juniorimportance: should knowfreq 55%

basics

~20 s

ForkJoinPool is a thread pool for divide-and-conquer work. You fork a big task into smaller subtasks that run in parallel, then join their results back together. It is built for CPU-bound recursive splitting, like processing halves of an array.

open as a page

How does ForkJoinPool differ from a fixed ThreadPoolExecutor, and when would you choose each?

level: middleimportance: should knowfreq 58%

basics

~20 s

A fixed ThreadPoolExecutor has one shared queue that all threads pull from, and is great for many independent tasks. ForkJoinPool gives each thread its own queue and lets idle threads steal work, which suits recursive divide-and-conquer tasks that spawn subtasks. Use the executor for independent jobs, Fork/Join for splitting work.

open as a page

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()?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Fork/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().

open as a page

How do you choose the splitting threshold (the size at which a Fork/Join task stops splitting and computes directly), and what happens if it's too small or too large?

level: seniorimportance: should knowfreq 45%

basics

~20 s

The threshold is the subtask size at which you stop splitting and just do the work directly (the base case). Too small means lots of tiny tasks whose fork/join overhead outweighs the work, so parallelism doesn't pay off. Too large means too few tasks to keep all CPU cores busy. Pick a size where the work per task clearly outweighs the splitting overhead, and tune by measuring.

open as a page

When you must perform blocking work inside a ForkJoinPool, how does ManagedBlocker prevent thread starvation?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

ForkJoinPool.ManagedBlocker lets you tell the pool that a worker is about to block. The pool then starts a temporary extra thread to keep the right number of threads actually running, so the blocked worker doesn't shrink the pool's parallelism and stall everyone else.

open as a page

When would you reach for raw RecursiveTask/RecursiveAction instead of a parallel stream, given both run on the Fork/Join framework?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Parallel streams are built on top of Fork/Join and handle the splitting and combining for you, so prefer them for most data-parallel jobs. Reach for raw RecursiveTask/RecursiveAction when you need control the stream can't give: a custom splitting strategy, recursing over a non-collection structure like a tree, a custom pool, or fine-grained threshold tuning.

open as a page