ForkJoinPool
2 mins
ForkJoinPool is a specialized implementation of ExecutorService designed for “divide-and-conquer” algorithms. It is the engine behind Java’s Parallel Streams and CompletableFuture.
Source Code #
Mechanism: Work-Stealing #
Unlike a standard ThreadPoolExecutor where threads share a single task queue, every worker thread in a ForkJoinPool has its own double-ended queue (deque) of tasks.
- LIFO locally: A thread pushes new subtasks onto the head of its own deque and pops them from the head (LIFO). This keeps the most recent data in the CPU cache.
- FIFO stealing: When a thread runs out of work, it looks at the deques of other threads and “steals” a task from the tail of their deque (FIFO). This balances the load across all available processors.
Canonical Usage #
When to use: Use ForkJoinPool for recursive tasks that can be broken into smaller subtasks (e.g., image processing, large array sorting, or tree traversal). For most cases, use the static ForkJoinPool.commonPool().
Common Patterns:
- RecursiveTask: For tasks that return a result.
- RecursiveAction: For tasks that do not return a result.
public class SumTask extends RecursiveTask<Long> {
private final long[] array;
private final int start, end;
protected Long compute() {
if (end - start < THRESHOLD) {
return computeDirectly();
} else {
int mid = (start + end) / 2;
SumTask left = new SumTask(array, start, mid);
SumTask right = new SumTask(array, mid, end);
left.fork(); // Run left in background
return right.compute() + left.join(); // Run right and wait for left
}
}
}
Performance Considerations #
- Pros: Outstanding utilization of multi-core CPUs for recursive work. Minimal contention between worker threads.
- Cons: Not suitable for tasks that block on I/O (unless using
ManagedBlocker). Overkill for simple, non-recursive task execution.