forkjointask.compute() 不自动递归,需显式调用 fork() 或 invoke() 才执行;阈值应依计算量设为100–1000,子任务须先 fork 再 join;forkjoinpool 窃取从队首取任务,适合纯 cpu 密集型无锁任务;recursiveaction 更轻量,异常直抛;commonpool 被 jdk 多处共享,业务应自建池避免干扰。

任务拆分不是递归越深越好:ForkJoinTask.compute() 的触发边界
拆分任务时,ForkJoinTask.compute() 不会自动递归调用自己;它只在你显式调用 fork() 或 invoke() 时才真正启动执行逻辑。很多人误以为只要重写 compute() 就会自动分治,结果发现任务根本没被拆——因为没调用 fork(),也没触发 join() 的等待链。
常见错误现象:compute() 里写了 if (size > threshold) { left.fork(); right.compute(); return left.join() + right.result; },但 left 实际没被提交到队列,fork() 后忘记 join() 或顺序错乱,导致结果为 0 或 NPE。
- 阈值
threshold要结合任务粒度和线程数估算,一般取 100–1000 次基础计算(比如数组遍历次数),不是固定写死10 - 必须确保子任务已
fork()(或invoke())后,再调用join(),否则可能拿到未完成结果 - 避免在
compute()中混用阻塞操作(如Thread.sleep()、文件读写),会卡住整个ForkJoinPool工作线程
工作窃取不是“自动负载均衡”:ForkJoinPool 的线程本地队列行为
ForkJoinPool 每个线程维护一个双端队列(deque),自己 push 任务到队尾,pop 从队尾取;而“窃取”是从其他线程队列的**队首**拿任务。这个设计是为了减少竞争,但也意味着:刚 fork 出的子任务如果还没被 push 完,窃取者可能拿不到,甚至看到空队列。
使用场景:适合 CPU 密集型、无锁、可预测耗时的任务。一旦任务中混入 I/O 或锁竞争,窃取机制就失效——线程卡在等锁,别的线程也偷不到活干。
立即学习“Java免费学习笔记(深入)”;
- 默认构造的
ForkJoinPool()使用ForkJoinPool.commonPool(),其并行度 =Runtime.getRuntime().availableProcessors() - 1,不是 CPU 核数 - 自定义池时,设置
parallelism过高(比如设成 64)反而因上下文切换拖慢整体速度 - 不要假设“任务被均匀分配”——短任务可能全挤在某个线程队列里,长任务又卡住一个线程,实际负载并不均衡
RecursiveAction vs RecursiveTask:返回值带来的额外开销与异常传播差异
用 RecursiveTask<t></t> 时,每次 join() 都会检查子任务是否抛出异常,并把第一个异常包装进 ExecutionException;而 RecursiveAction 没有返回值,异常只在 compute() 执行中直接抛出,不会被池捕获封装。
性能影响:如果你的任务本就不需要返回值,硬套 RecursiveTask<void></void>,不仅多一次装箱/拆箱,还让异常处理路径变复杂,join() 可能意外吞掉本该立即暴露的 NullPointerException。
- 纯副作用任务(比如批量更新数组元素)用
RecursiveAction,更轻量、异常更直给 - 需要聚合结果的任务(求和、查找最大值)才用
RecursiveTask,且记得在compute()开头加空值校验,别依赖join()帮你兜底 -
invokeAll(task1, task2)是安全的批量提交方式,比手动fork()+join()更少出错,但内部仍按顺序尝试窃取
commonPool() 不是万能默认池:自定义 ForkJoinPool 的必要条件
几乎所有 JDK 内部 API(比如 Arrays.parallelSort()、CompletableFuture 的 async 方法)都悄悄用 ForkJoinPool.commonPool()。这意味着:你的业务代码如果也用它,就会和这些系统调用共享同一组线程——一旦某个任务跑太久或阻塞,整个 commonPool 就卡住,连 CompletableFuture.delayedExecutor() 都可能延迟触发。
容易踩的坑:线上服务启用了 parallelStream(),但没意识到它背后是 commonPool;某次日志组件升级后开始用 CompletableFuture 异步刷盘,结果和业务 parallelStream 抢线程,CPU 拉满但吞吐不升反降。
- Web 应用、RPC 服务务必创建独立
ForkJoinPool,例如new ForkJoinPool(4),避免污染 commonPool - 池关闭需主动调用
shutdown()+awaitTermination(),不能靠 GC 回收——线程不会自动销毁 - commonPool 无法设置
UncaughtExceptionHandler,异常日志可能静默丢失;自定义池可以,便于排查compute()中未捕获的 RuntimeException
真正难的不是写对 fork() 和 join(),而是判断当前任务是否真的适合 Fork/Join——它对数据局部性、任务独立性、无共享状态的要求,比多数人想象中更苛刻。










