ForkJoinPool 详解

ForkJoinPool 是 JDK7 引入的分支 / 合并(Fork/Join) 线程池,专门用于可以递归拆分的任务 ,核心思想:分而治之。

适用场景:任务可以拆成多个子任务,子任务合并结果;典型:递归遍历、并行排序、大数据集合计算。 不适合:IO 阻塞型任务(数据库、网络调用),会严重浪费线程。

一、核心思想:Fork & Join

  1. fork(拆分):把大任务,切割成若干小的子任务,交给线程池并行执行;
  2. join(合并):等待所有子任务执行完成,收集子任务结果,合并成最终结果。

类比:统计 1000 万条数据求和 → 拆成 10 个 100 万的子任务并行计算,最后把 10 个结果相加。

二、核心类

表格

类 作用
ForkJoinPool 线程池主体,管理工作线程、任务队列
ForkJoinTask<V> 任务抽象类,相当于 Runnable/Callable,支持 fork/join
RecursiveTask<V> 有返回值的任务(最常用),重写 compute()
RecursiveAction 无返回值 任务,重写 compute()
CountedCompleter<T> JDK8 新增,完成回调链式任务,适合异步流水线

RecursiveTask 模板代码(求和示例)

复制代码
class SumTask extends RecursiveTask<Long> {
    private long[] arr;
    private int start;
    private int end;
    // 阈值:小于该值就不再拆分,直接串行计算
    private static final int THRESHOLD = 1000;

    public SumTask(long[] arr, int start, int end) {
        this.arr = arr;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        // 任务足够小,直接计算
        if (end - start <= THRESHOLD) {
            long sum = 0;
            for (int i = start; i < end; i++) {
                sum += arr[i];
            }
            return sum;
        }
        // 拆分任务
        int mid = (start + end) / 2;
        SumTask left = new SumTask(arr, start, mid);
        SumTask right = new SumTask(arr, mid, end);
        // fork 提交子任务
        left.fork();
        right.fork();
        // join等待子任务结果并合并
        return left.join() + right.join();
    }
}

// 使用
public static void main(String[] args) {
    long[] array = new long[20000];
    Arrays.fill(array, 1);
    ForkJoinPool pool = new ForkJoinPool();
    Long result = pool.invoke(new SumTask(array, 0, array.length));
    System.out.println(result);
    pool.shutdown();
}

三、核心机制:工作窃取 Work-Stealing

这是 ForkJoinPool 和普通 ThreadPoolExecutor 最大区别

  1. 每个工作线程(WorkerThread) 都有一个双端队列 Deque;
  2. 线程优先处理自己队列里的任务;
  3. 当自己队列空了,去其他线程的队列尾部偷任务(减少竞争,从尾部拿,对方从头部取);
  4. 目的:最大化 CPU 利用率,避免线程空闲。

普通线程池:所有线程共用一个任务队列,多线程抢同一个队列,锁竞争高。

⚠️ 注意:队列存放的是 ForkJoinTask,不是普通 Runnable。

四、ForkJoinPool 构造器与参数

复制代码
public ForkJoinPool(int parallelism,
                    ForkJoinWorkerThreadFactory factory,
                    UncaughtExceptionHandler handler,
                    boolean asyncMode)
  • parallelism:并行度 ,默认 Runtime.getRuntime().availableProcessors(),不等于最大线程数,代表期望并行数量;
  • factory:创建 worker 线程工厂;
  • handler:任务异常处理器;
  • asyncMode:是否异步模式。
    • false(默认,FIFO):队列 LIFO,适合递归 fork/join 任务;
    • true:FIFO,适合不 join 的异步事件任务。

JDK8 新增 ForkJoinPool.commonPool() 公共池

复制代码
ForkJoinPool commonPool = ForkJoinPool.commonPool();
  • 全局共享,不要 shutdown ;Stream 并行流 parallelStream() 默认使用这个公共池;
  • 并行度默认 = CPU 核心数 -1;
  • 风险:如果在 commonPool 执行阻塞任务,会占满公共池,影响所有使用并行流的代码。

五、提交任务 4 种方式

  1. invoke(task):阻塞,等待任务完成,返回结果;
  2. submit(task):非阻塞,返回 ForkJoinTask,后续用get()/join()拿结果;
  3. execute(task):无返回,异步执行;
  4. commonPool() 静态提交。

join() vs get()

  • join():ForkJoinTask 专属,抛出运行时异常 CompletionException,不需要显式 catch 受检异常;
  • get():继承 Future,抛出受检异常InterruptedException, ExecutionException;

推荐在 ForkJoin 任务内部用join()。

六、任务拆分的阈值(THRESHOLD)怎么选?

阈值不是越小越快:

  • 阈值太小 → 任务拆分过多,创建大量 ForkJoinTask 对象,对象开销 > 并行收益;
  • 阈值太大 → 拆分太少,并行度不足,CPU 跑不满;

经验:根据任务计算耗时测试调整,CPU 密集型任务,单任务执行 1ms~10ms 比较合适。

七、重要特性 & 坑点

✅ 优点

  1. 工作窃取,CPU 利用率高;
  2. 天然支持递归分治任务;
  3. JDK8 parallelStream 底层基于 common ForkJoinPool。

❌ 缺点与坑

  1. 只适合 CPU 密集型 ,不能做 IO 阻塞任务(数据库、http) 线程阻塞后,工作窃取机制不会自动扩容线程,线程被卡住,并行度下降。
  2. commonPool全局共享,一个任务阻塞,影响整个应用并行流;
  3. 任务拆分有开销,简单小任务不要用 ForkJoin,串行更快;
  4. 递归深度过大,会创建大量任务,内存上涨;
  5. 异常处理:子任务异常会在 join/get 时抛出,异常会向上传播;
  6. 不要在 ForkJoin 任务内部直接新建线程。

八、ForkJoinPool vs ThreadPoolExecutor

表格

特性 ForkJoinPool ThreadPoolExecutor
任务模型 分治递归,支持 fork/join 独立任务
队列 每个线程私有双端队列 + 工作窃取 全局共享阻塞队列
适用场景 CPU 密集、可拆分任务(并行流) 通用场景,IO/CPU 都可
线程管理 并行度,动态窃取任务 核心线程、最大线程,队列排队

九、JDK8 Stream parallelStream 注意

复制代码
list.parallelStream().forEach(xxx);

底层就是 ForkJoinPool.commonPool()

坑:并行流里不要操作非线程安全变量(ArrayList 等),会并发修改异常; 如果需要自定义线程池执行 parallelStream:不能直接给 stream 设置池,需要把并行流代码包装在自定义 ForkJoinPool 任务里执行。

十、简单性能结论

  • 任务是 CPU 密集、可以拆分 → ForkJoinPool 优于普通线程池;
  • IO 任务、短任务、不可拆分任务 → 普通 ThreadPoolExecutor 更合适。
相关推荐
Wang's Blog1 小时前
Java 项目实战: 交友项目 - Dubbo 启动检查
java·dubbo·交友
零基础1231 小时前
权限管理怎么做?从“资格”与“空间”双维度设计一套可落地的权限体系
java·服务器·数据库
starzy19901 小时前
Flink Task 调度任务深度剖析:从调度策略到故障恢复的完整链路
java·大数据·flink
开开心心_Every1 小时前
文件查重软件批量删除重复文件释放空间
java·开发语言·c#·计算机外设·ocr·excel·perl
Wang's Blog1 小时前
Java 项目实战: 交友项目 - 技术架构与选型剖析
java·架构·交友
我是大猴子2 小时前
一些八股和限流措施
java
认真的小羽❅2 小时前
ThreadLocal 在虚拟线程间要小心
java·开发语言·jvm
美味蛋炒饭.2 小时前
Java八股面试(4)
java·开发语言·面试
SL_staff2 小时前
OKR落地难?技术团队如何用五层联动链打通执行断层
java·开源·全栈