Definition
ForkJoinTask– base type for divide-and-conquer tasks in aForkJoinPool.RecursiveTask– returns a result of type V.RecursiveAction– returns nothing (side-effect only).fork()– schedules a subtask asynchronously onto the worker's deque.compute()– runs the task's body directly on the current thread.join()– waits for a forked subtask and returns its result.- Sequential threshold – the size below which a task solves directly instead of splitting further.
National Bank of India must total the interest on a million accounts. One clerk is too slow. The head clerk splits the stack in half, hands one half to a colleague (fork), works the other half himself (compute), and when both are done adds the subtotals (join). Each colleague does the same until a pile is small enough to add by hand (threshold).
Understanding the Concepts
How do you implement a RecursiveTask?
RecursiveTask returns a result. The compute method checks if the work is small enough to solve directly (below the threshold). If not, it splits the work, forks one half, computes the other half directly, and joins the result.
package com.bank.parallel;
import java.math.BigDecimal;
import java.util.concurrent.RecursiveTask;
public class InterestSumTask extends RecursiveTask<BigDecimal> {
private static final int THRESHOLD = 10_000;
private final BigDecimal[] interests;
private final int from;
private final int to;
public InterestSumTask(BigDecimal[] interests, int from, int to) {
this.interests = interests;
this.from = from;
this.to = to;
}
@Override
protected BigDecimal compute() {
if (to - from <= THRESHOLD) {
// Sequential: below threshold, solve directly
BigDecimal sum = BigDecimal.ZERO;
for (int i = from; i < to; i++) {
sum = sum.add(interests[i]);
}
return sum;
}
int mid = (from + to) / 2;
InterestSumTask left = new InterestSumTask(interests, from, mid);
InterestSumTask right = new InterestSumTask(interests, mid, to);
left.fork(); // schedule left asynchronously
BigDecimal rightResult = right.compute(); // compute right directly
BigDecimal leftResult = left.join(); // wait for left
return leftResult.add(rightResult);
}
}
package com.bank.parallel;
import java.math.BigDecimal;
import java.util.concurrent.ForkJoinPool;
public class InterestCalculationMain {
public static void main(String[] args) {
BigDecimal[] interests = new BigDecimal[1_000_000];
for (int i = 0; i < interests.length; i++) {
interests[i] = new BigDecimal("12.50"); // sample daily interest
}
ForkJoinPool pool = ForkJoinPool.commonPool();
BigDecimal total = pool.invoke(new InterestSumTask(interests, 0, interests.length));
System.out.println("Total interest: " + total);
}
}
What is the fork-compute-join idiom?
Always fork one side and compute the other yourself. This keeps the current thread busy instead of idle. If you fork both sides and join both, the current thread sits idle waiting for the first join, wasting a core.
// WRONG: fork both, current thread idles during left.join()
left.fork();
right.fork();
return left.join().add(right.join());
// CORRECT: fork one, compute the other yourself
left.fork();
BigDecimal rightResult = right.compute(); // current thread stays busy
return left.join().add(rightResult);
How does work stealing work?
Each worker has a double-ended queue (deque). It pushes and pops its own subtasks from one end (LIFO). When idle, it steals from the other end (FIFO) of a busy worker's queue. Stealing from the opposite end minimizes contention between the owner and the thief.
What is the right threshold?
Too small: millions of tiny tasks, and the overhead of creating and scheduling tasks dwarfs the actual work. Too large: not enough parallelism to keep all cores busy. A good threshold keeps each leaf task doing a meaningful chunk of CPU work. For array operations, 1000 to 10000 elements per leaf is typical.
How do you implement a RecursiveAction?
RecursiveAction returns nothing (void). It is used for side-effect-only work like parallel logging or parallel file processing.
package com.bank.parallel;
import java.math.BigDecimal;
import java.util.concurrent.RecursiveAction;
public class InterestApplyAction extends RecursiveAction {
private static final int THRESHOLD = 10_000;
private final BigDecimal[] balances;
private final BigDecimal rate;
private final int from;
private final int to;
public InterestApplyAction(BigDecimal[] balances, BigDecimal rate, int from, int to) {
this.balances = balances;
this.rate = rate;
this.from = from;
this.to = to;
}
@Override
protected void compute() {
if (to - from <= THRESHOLD) {
for (int i = from; i < to; i++) {
balances[i] = balances[i].add(balances[i].multiply(rate));
}
return;
}
int mid = (from + to) / 2;
invokeAll(
new InterestApplyAction(balances, rate, from, mid),
new InterestApplyAction(balances, rate, mid, to)
);
}
}
When does fork/join not help?
For small inputs, a sequential loop is faster because fork/join has fixed overhead for task creation and scheduling. For blocking IO, the pool threads sit idle and cannot participate in work stealing. Fork/join is strictly for CPU-bound, splittable problems.
What are common interview follow-up questions?
- What is the default common pool parallelism?
Runtime.getRuntime().availableProcessors() - 1. - How does ManagedBlocker help with blocking tasks? It tells the pool to compensate for a blocked thread by temporarily adding another worker.
- How do parallel streams use ForkJoinPool internally? Parallel streams submit Spliterator-based tasks to the common pool, using the same fork/join mechanism.
Visual Overview
Fork/Join Divide-and-Conquer:
[0 ──────────────────────── 1,000,000]
│ │
▼ ▼
[0 ─────── 500,000] [500,000 ── 1,000,000]
│ │ │ │
▼ ▼ ▼ ▼
[0-250K] [250K-500K] [500K-750K] [750K-1M]
│ │ │ │
if <= THRESHOLD: compute sequentially
else: fork left, compute right, join left
Work Stealing:
Worker 1 deque: [task] [task] ←── pushes here
Worker 2 (idle): steals from ──▶ other end
Key Takeaway
Fork/join uses RecursiveTask or RecursiveAction with fork, compute, and join to divide a CPU-bound problem across cores via work stealing. The sequential threshold determines when to stop splitting; too small creates task overhead, too large underuses cores.
At National Bank of India our end-of-day interest sum across millions of accounts used a RecursiveTask with a threshold of ten thousand elements, giving a clean speedup while matching the sequential total exactly.
Remember one sentence: fork one side, compute the other yourself, join, and set a sequential threshold so splitting overhead never outweighs the benefit.
---