ForkJoinPool是 JDK7 引入的分支 / 合并(Fork/Join)线程池,专门用于可以递归拆分的任务,核心思想:分而治之。
适用场景:任务可以拆成多个子任务,子任务合并结果;典型:递归遍历、并行排序、大数据集合计算。 不适合:IO 阻塞型任务(数据库、网络调用),会严重浪费线程。
一、核心思想:Fork & Join
- fork(拆分):把大任务,切割成若干小的子任务,交给线程池并行执行;
- 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最大区别
- 每个工作线程(WorkerThread)都有一个双端队列 Deque;
- 线程优先处理自己队列里的任务;
- 当自己队列空了,去其他线程的队列尾部偷任务(减少竞争,从尾部拿,对方从头部取);
- 目的:最大化 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 种方式
invoke(task):阻塞,等待任务完成,返回结果;submit(task):非阻塞,返回ForkJoinTask,后续用get()/join()拿结果;execute(task):无返回,异步执行;commonPool()静态提交。
join() vs get()
join():ForkJoinTask 专属,抛出运行时异常 CompletionException,不需要显式 catch 受检异常;get():继承 Future,抛出受检异常InterruptedException, ExecutionException;
推荐在 ForkJoin 任务内部用
join()。
六、任务拆分的阈值(THRESHOLD)怎么选?
阈值不是越小越快:
- 阈值太小 → 任务拆分过多,创建大量 ForkJoinTask 对象,对象开销 > 并行收益;
- 阈值太大 → 拆分太少,并行度不足,CPU 跑不满;
经验:根据任务计算耗时测试调整,CPU 密集型任务,单任务执行 1ms~10ms 比较合适。
七、重要特性 & 坑点
✅ 优点
- 工作窃取,CPU 利用率高;
- 天然支持递归分治任务;
- JDK8 parallelStream 底层基于 common ForkJoinPool。
❌ 缺点与坑
- 只适合 CPU 密集型,不能做 IO 阻塞任务(数据库、http)
线程阻塞后,工作窃取机制不会自动扩容线程,线程被卡住,并行度下降。
commonPool全局共享,一个任务阻塞,影响整个应用并行流;- 任务拆分有开销,简单小任务不要用 ForkJoin,串行更快;
- 递归深度过大,会创建大量任务,内存上涨;
- 异常处理:子任务异常会在 join/get 时抛出,异常会向上传播;
- 不要在 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 更合适。