Definition
- Work-stealing – each worker thread owns a double-ended queue (deque). The worker pushes forked subtasks onto and pops tasks from the FRONT of its deque (LIFO). An idle worker steals tasks from the BACK of another worker's deque (FIFO).
- LIFO for owner – the most recently forked task is processed first. This subtask likely shares data with the parent, so it is hot in the CPU cache.
- FIFO for thief – the oldest task is stolen first. Older tasks tend to be higher up in the recursion tree and represent larger subtrees with more work, giving the thief more to do.
- Opposite ends – owner and thief operate at opposite ends of the deque, so they rarely contend for the same lock.
- Common pool –
ForkJoinPool.commonPool()is a JVM-wide singleton used byparallelStream(),CompletableFuture.supplyAsync()(no explicit executor), and many frameworks. Its parallelism isavailableProcessors() - 1. - Blocking danger – if common pool workers block on IO, they cannot steal or process tasks. Other parts of the application that rely on the common pool (any parallel stream, any default CompletableFuture) stall behind the blocked workers.
Each teller at National Bank of India has their own stack of forms (deque). Rajesh works from the top of his stack. When his stack is empty, he goes to Priya's desk and takes forms from the bottom of her stack. Top versus bottom means they rarely bump into each other. The common pool is like 3 shared tellers used by every department. If all 3 are stuck on long phone calls (blocking IO), no forms get processed anywhere in the bank.
Understanding the Concepts
How does work-stealing work?
Work-stealing is a load-balancing strategy where idle threads take work from busy threads. In ForkJoinPool, each worker has a double-ended queue. The owner pushes and pops from the front (LIFO). Thieves steal from the back (FIFO). This design keeps all cores busy with minimal synchronization overhead.
package com.bank.concurrency;
import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;
import java.math.BigDecimal;
public class SumTransactionsTask extends RecursiveTask<BigDecimal> {
private final BigDecimal[] amounts;
private final int lo, hi;
private static final int THRESHOLD = 1000;
SumTransactionsTask(BigDecimal[] amounts, int lo, int hi) {
this.amounts = amounts;
this.lo = lo;
this.hi = hi;
}
@Override
protected BigDecimal compute() {
if (hi - lo <= THRESHOLD) {
BigDecimal sum = BigDecimal.ZERO;
for (int i = lo; i < hi; i++) sum = sum.add(amounts[i]);
return sum;
}
int mid = (lo + hi) >>> 1;
SumTransactionsTask left = new SumTransactionsTask(amounts, lo, mid);
left.fork(); // push onto my deque
SumTransactionsTask right = new SumTransactionsTask(amounts, mid, hi);
BigDecimal rightResult = right.compute(); // compute right now
BigDecimal leftResult = left.join(); // get left result
return leftResult.add(rightResult);
}
public static void main(String[] args) {
BigDecimal[] amounts = new BigDecimal[1_000_000];
for (int i = 0; i < amounts.length; i++)
amounts[i] = new BigDecimal("100.00");
ForkJoinPool pool = new ForkJoinPool();
BigDecimal total = pool.invoke(
new SumTransactionsTask(amounts, 0, amounts.length));
System.out.println("Total: " + total);
}
}
Why is blocking IO on the common pool dangerous?
The common pool is sized to approximately the number of CPU cores. If workers block on IO, they cannot process tasks or steal from other workers. Every parallel stream and default CompletableFuture in the entire application shares this pool.
package com.bank.concurrency;
import java.util.stream.IntStream;
public class CommonPoolDangerDemo {
public static void main(String[] args) {
// DANGEROUS: blocking HTTP calls on the common pool
IntStream.range(0, 100).parallel().forEach(i -> {
// Each worker blocks for 200ms on HTTP call
// All common pool workers stuck!
// parallelStream in OTHER parts of the app: STALLED
// CompletableFuture.supplyAsync(): STALLED
httpCall("http://api.bank.com/balance/" + i);
});
}
static String httpCall(String url) {
try { Thread.sleep(200); } catch (InterruptedException e) { }
return "response";
}
}
How do you fix the common pool blocking problem?
Use a dedicated bounded ThreadPoolExecutor for IO-bound work. Keep the common pool reserved for CPU-bound computation.
package com.bank.concurrency;
import java.util.concurrent.*;
public class DedicatedPoolDemo {
private static final ExecutorService ioPool =
Executors.newFixedThreadPool(50); // dedicated IO pool
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
final int id = i;
CompletableFuture.runAsync(
() -> httpCall("http://api.bank.com/balance/" + id),
ioPool); // explicit executor, NOT common pool
}
// Common pool: free for CPU-bound parallelStream
}
static void httpCall(String url) {
try { Thread.sleep(200); } catch (InterruptedException e) { }
}
}
What is ManagedBlocker?
ForkJoinPool has a compensation mechanism: when a worker blocks through a ManagedBlocker interface, the pool may create a temporary worker to maintain parallelism. However, this is not a reliable solution for bulk IO because it can create many compensation threads.
What are common interview follow-up questions?
- Why LIFO for the owner? The most recently forked task shares data with the parent task, making it hot in the CPU cache. Processing it first exploits temporal locality.
- Why FIFO for the thief? The oldest task is higher in the recursion tree and represents a larger subtree with more work, which minimizes the number of steal operations needed.
Visual Overview
Work-Stealing in ForkJoinPool
═════════════════════════════
Worker-1 deque: Worker-2 deque:
FRONT → [D][C][B][A] FRONT → []
│ │
Worker-1 pops D (LIFO) Worker-2 steals A from Worker-1 (FIFO)
│ │
FRONT → [C][B] FRONT → [A]
Owner: LIFO (front) → cache locality
Thief: FIFO (back) → largest subtree
Opposite ends → minimal contention
Key Takeaway
ForkJoinPool uses work-stealing to keep all CPU cores busy. Each worker has its own deque. The owner processes tasks from the front (LIFO for cache locality). Idle workers steal from the back of busy workers' deques (FIFO for the largest subtasks). Operating at opposite ends minimizes lock contention.
The common pool is shared by parallelStream and CompletableFuture.supplyAsync(). Blocking IO on it is dangerous because blocked workers cannot steal or process tasks, causing all parallel streams and CompletableFutures in the entire application to stall. The fix is a dedicated bounded ThreadPoolExecutor for IO work, keeping the common pool free for CPU-bound computation.
At National Bank of India, a VaR (Value at Risk) computation using parallelStream stalled for 500 milliseconds because market data HTTP calls blocked common pool workers. After segregating IO onto a dedicated pool of 30 threads, VaR computation dropped to 50 milliseconds and the common pool was always available for CPU work.
Remember one sentence: work-stealing keeps cores busy by letting idle workers take tasks from busy workers' deques, but blocking IO on the shared common pool starves the entire application.
---