skip to content

RecursiveTask, RecursiveAction & Splitting

RecursiveTask returns a result and RecursiveAction does not, and the idiomatic body forks one half then computes the other before joining. The judgment call interviewers probe is the splitting threshold: too small and fork/join overhead eats the gain.

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

questions

5

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

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