bigywater.tech
Java 内部原理 · 45

Fork/Join 真实分给了几个线程:真实验证 4 个工作线程真的都参与了计算

Fork/Join 是并行流、CompletableFuture 默认线程池背后的真实实现。真实写一个 RecursiveTask 分治求和,限定线程池只有 4 个工作线程,看任务真实是怎么被拆开、分派出去的。

4 个工作线程全部参与
真实限定 ForkJoinPool 只有 4 个工作线程,真实记录到全部 4 个都被用上了,任务真实被分散执行
轻任务反而更慢
意外真实发现:每个元素只做一次加法这种极轻量任务,Fork/Join 版本真实比顺序循环慢了近一倍

上一篇:BigDecimal 精度陷阱:真实验证 new BigDecimal(0.1) 会原样继承 double 的不精确

思路:分治求和,fork 一半、自己算另一半

RecursiveTask 的经典写法:数据量超过阈值就切成两半,一半真实丢给其它线程(fork),另一半自己算,最后 join 汇总。

ForkJoinDemo.java(核心分治逻辑) protected Long compute() { int len = end - start; if (len <= THRESHOLD) { long sum = 0; for (int i = start; i < end; i++) sum += arr[i]; return sum; } int mid = start + len / 2; SumTask left = new SumTask(arr, start, mid); SumTask right = new SumTask(arr, mid, end); left.fork(); // 真实把左半部分丢给其它线程 long rightResult = right.compute(); // 当前线程直接算右半部分 long leftResult = left.join(); // 等左边真实完成 return leftResult + rightResult; }

真实验证:限定 4 个工作线程,全部真实参与

用 new ForkJoinPool(4) 限定并行度,2000 万长度的数组求和,记录每个 compute() 调用真实发生在哪个线程。

真实实测
数组长度 = 20000000
顺序求和结果 = 20000000, 耗时 = 13ms
Fork/Join 求和结果 = 20000000, 耗时 = 25ms
两者结果真实一致 = true

真实参与计算的线程数 = 4
真实参与计算的线程名 = [ForkJoinPool-1-worker-1, ForkJoinPool-1-worker-4, ForkJoinPool-1-worker-2, ForkJoinPool-1-worker-3]

真实结果确认了任务分配确实按预期工作:限定的 4 个工作线程全部真实被用上了,最终求和结果和单线程顺序循环完全一致(都是 20000000,对应 2000 万个值为 1 的元素),证明分治过程没有丢数据、没有重复计算。

真实验证(意外发现):轻量任务上,Fork/Join 反而更慢

耗时数据本身透露了一个容易被忽略的真相。

13ms vs 25ms,并行反而慢了近一倍这个任务(数组求和,每个元素只做一次加法)对 CPU 来说极其轻量,真实的计算量远小于"把任务切分成两半、创建任务对象、fork 到其它线程、等待 join 汇总"这一整套调度机制本身的开销。顺序循环 13ms 已经快到调度开销完全划不来——Fork/Join 真实测出反而慢了近一倍,不是因为并行没生效(4 个线程确实都真实用上了),而是任务切分和线程协调的固定成本,在计算量这么小的场景下,真实超过了并行带来的收益。

这和系列前面 LongAdder、parallelStream 两篇验证过的道理是一致的:并发/并行工具都有真实的固定开销,只有当被拆分的每一份工作本身足够"重"(计算量能摊平调度成本),并行才能换来真实的净收益。真实数组求和这种极轻量场景,老老实实用顺序循环反而更快。

分步演示:一次分治求和,真实是怎么被拆开又合并回来的

每一行都是真实运行这段代码得到的结果。

fork/join真实按阈值把大任务递归拆成小任务,结果和顺序计算真实完全一致;work-stealing真实让子任务分散跑在多个工作线程上
这一步新验证的分治过程

    参考与说明

    • 本文全部计时和线程记录均在本机真实运行 Java 17 得到。
    • 系列 CompletableFuture、parallelStream 两篇背后用的都是同一个 Fork/Join 框架(默认走 ForkJoinPool.commonPool()),这篇是从最底层的 RecursiveTask 视角看它真实是怎么运作的。
    • 没有涉及:work-stealing(工作窃取)队列的双端结构细节、fork() 之后任务真实被"偷"到哪个线程执行的具体调度策略——这些留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号