Home/Learn/Java A–Z/Fork/Join Framework

Fork/Join Framework

Advanced
Concurrency

The Fork/Join framework efficiently parallelises divide-and-conquer algorithms using a work-stealing thread pool.

Overview

The Fork/Join framework (java.util.concurrent.ForkJoinPool, introduced Java 7) is designed for recursive, divide-and-conquer algorithms. Tasks split themselves into smaller subtasks (fork), run them in parallel, then combine results (join). ForkJoinPool uses work-stealing — idle threads steal tasks from busy threads' queues — maximising CPU utilisation. Parallel streams internally use ForkJoinPool.commonPool().

RecursiveTask and RecursiveAction

Extend RecursiveTask<V> for tasks that return a result (like a parallel sum). Extend RecursiveAction for tasks with no result (like parallel sort or array fill).

The pattern: if the problem is small enough (sequential threshold), solve directly. Otherwise fork two subtasks, join both, combine results.

RecursiveTask.java
import java.util.concurrent.*;

// RecursiveTask — returns a result
public class ParallelSum extends RecursiveTask<Long> {
    private static final int THRESHOLD = 10_000;
    private final long[] array;
    private final int start, end;

    public ParallelSum(long[] array, int start, int end) {
        this.array = array; this.start = start; this.end = end;
    }

    @Override
    protected Long compute() {
        int length = end - start;
        if (length <= THRESHOLD) {
            // Base case — sequential
            long sum = 0;
            for (int i = start; i < end; i++) sum += array[i];
            return sum;
        }
        // Divide
        int mid = start + length / 2;
        ParallelSum left  = new ParallelSum(array, start, mid);
        ParallelSum right = new ParallelSum(array, mid,   end);

        left.fork();             // schedule left async
        long rightResult = right.compute(); // run right inline
        long leftResult  = left.join();     // wait for left

        return leftResult + rightResult;   // combine
    }
}

// Run it
ForkJoinPool pool = ForkJoinPool.commonPool();
long[] data = LongStream.range(0, 1_000_000).toArray();
long total = pool.invoke(new ParallelSum(data, 0, data.length));

Work Stealing

Each thread in ForkJoinPool has its own double-ended deque (deque). New subtasks are pushed to the thread's own deque (LIFO — locality). Idle threads steal from the tail of other threads' deques (FIFO — least recently added = largest tasks first).

This is far more efficient than a shared work queue under recursive parallelism because it reduces contention and keeps threads busy.

WorkStealing.java
/*
  Work-stealing visualisation:

  Thread 1 deque:  [Task A, A1, A2, A3]   ← pushes new subtasks here (top)
  Thread 2 deque:  [Task B, B1]            ← Thread 2 pushes here
  Thread 3 deque:  []                      ← Thread 3 is idle

  Thread 3 STEALS from Thread 1's tail:   steals Task A (oldest/largest)
  Now Thread 3 works on Task A while Thread 1 works on A1, A2, A3

  Benefits:
  • No contention on a shared queue
  • Larger tasks stolen first (good split granularity)
  • Idle threads find work automatically
*/

// ForkJoinPool configuration
ForkJoinPool customPool = new ForkJoinPool(
    4,                            // parallelism (default: # CPU cores)
    ForkJoinPool.defaultForkJoinWorkerThreadFactory,
    null,                         // uncaught exception handler
    true                          // asyncMode (FIFO for non-recursive tasks)
);

// Run task in custom pool (avoid polluting common pool)
long result = customPool.invoke(new ParallelSum(data, 0, data.length));
customPool.shutdown();

Fork/Join vs Parallel Streams

Parallel streams use ForkJoinPool.commonPool() internally — they are Fork/Join under the hood. For most use cases, parallel streams are the right abstraction.

Use raw Fork/Join when: you need custom work-stealing behaviour, your task structure is irregular, you need fine-grained control over splitting, or you want to avoid sharing the common pool.

StreamVsForkJoin.java
// Parallel stream — Fork/Join under the hood (easier API)
long sum = LongStream.range(0, 1_000_000)
    .parallel()
    .sum();

// Equivalent Fork/Join (more verbose, more control)
ForkJoinPool.commonPool().invoke(new ParallelSum(data, 0, data.length));

// CAUTION: don't block in common pool tasks — it starves other streams
// Bad:
long result = IntStream.range(0, 100)
    .parallel()
    .mapToLong(i -> {
        Thread.sleep(100); // blocks a common pool thread!
        return expensiveIO(i);
    }).sum();

// Better: use a custom pool for I/O-bound parallel work
ForkJoinPool ioPool = new ForkJoinPool(32);
long result2 = ioPool.submit(() ->
    IntStream.range(0, 100).parallel()
        .mapToLong(i -> expensiveIO(i))
        .sum()
).get();

Key Points to Remember

  • RecursiveTask<V> returns a result; RecursiveAction returns void.
  • Pattern: if small → solve directly; else fork + join two halves.
  • fork() submits a subtask asynchronously; join() blocks until it completes.
  • Work-stealing: idle threads steal from tails of busy threads' deques.
  • Parallel streams use ForkJoinPool.commonPool() — avoid blocking I/O inside parallel stream operations.

Practice Fork/Join Framework in the Playground

Run and modify code directly in your browser - no setup needed.

Interview Questions

Sign in to ask Aria
1

What is the difference between RecursiveTask and RecursiveAction?

EasyOracle
2

Explain work-stealing in ForkJoinPool.

MediumAmazon
3

When should you use Fork/Join instead of parallel streams?

MediumGoogle
4

What is the sequential threshold in Fork/Join and how do you choose it?

HardMicrosoft
5

Why is blocking I/O inside a parallel stream dangerous?

MediumNetflix

Ask Aria about Fork/Join Framework

Your personal AI tutor — ask anything about this concept