skip to content

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