ForkJoin and work stealing

Multithreading · lesson 22 of 38 · 4 min read

Split a big CPU task into halves that idle workers can steal, then join the answers back together.

Open this lesson in the learning hub

Key points

  • Fork/join is divide and conquer: split until a piece is small enough, compute it, then join the pieces back up.
  • Every worker has its own deque. An idle worker steals from the tail of a busy one, so no core sits still.
  • The pattern is fork one half, compute the other half yourself, then join. Forking both halves wastes a thread.
  • Choose a threshold where the work costs more than the split. A few thousand elements is a normal starting point.
  • It is built for CPU-bound work. Blocking inside a fork/join task starves every other task in the pool.
  • ForkJoinPool.commonPool() is shared by the whole JVM, and it is exactly what parallel streams use.

Example

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

public class Main {
    static class SumTask extends RecursiveTask<Long> {
        private static final int THRESHOLD = 5_000;        // below this, just do the work
        private final long[] data;
        private final int from, to;

        SumTask(long[] data, int from, int to) { this.data = data; this.from = from; this.to = to; }

        @Override protected Long compute() {
            if (to - from <= THRESHOLD) {
                long sum = 0;
                for (int i = from; i < to; i++) sum += data[i];
                return sum;
            }
            int mid = (from + to) >>> 1;
            SumTask left = new SumTask(data, from, mid);
            left.fork();                                        // queued, an idle worker may steal it
            long right = new SumTask(data, mid, to).compute();  // do one half yourself
            return right + left.join();                         // then wait for the other half
        }
    }

    public static void main(String[] args) {
        long[] data = new long[100_000];
        for (int i = 0; i < data.length; i++) data[i] = i + 1;

        ForkJoinPool pool = ForkJoinPool.commonPool();
        long total = pool.invoke(new SumTask(data, 0, data.length));

        System.out.println("sum         : " + total);
        System.out.println("expected    : " + (100_000L * 100_001L / 2));
        System.out.println("parallelism : " + pool.getParallelism());
        System.out.println("steals      : " + pool.getStealCount() + " (idle workers took queued halves)");
    }
}

Split until small, compute one half yourself, join the rest.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Multithreading course, and every lesson in it is listed on the Multithreading contents page.