Fork/Join Framework
AdvancedThe 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.
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.
/*
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.
// 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 AriaWhat is the difference between RecursiveTask and RecursiveAction?
Explain work-stealing in ForkJoinPool.
When should you use Fork/Join instead of parallel streams?
What is the sequential threshold in Fork/Join and how do you choose it?
Why is blocking I/O inside a parallel stream dangerous?
Ask Aria about Fork/Join Framework
Your personal AI tutor — ask anything about this concept