Fork/Join 是并行流、CompletableFuture 默认线程池背后的真实实现。真实写一个 RecursiveTask 分治求和,限定线程池只有 4 个工作线程,看任务真实是怎么被拆开、分派出去的。
上一篇:BigDecimal 精度陷阱:真实验证 new BigDecimal(0.1) 会原样继承 double 的不精确
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; }
用 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 的元素),证明分治过程没有丢数据、没有重复计算。
耗时数据本身透露了一个容易被忽略的真相。
这和系列前面 LongAdder、parallelStream 两篇验证过的道理是一致的:并发/并行工具都有真实的固定开销,只有当被拆分的每一份工作本身足够"重"(计算量能摊平调度成本),并行才能换来真实的净收益。真实数组求和这种极轻量场景,老老实实用顺序循环反而更快。
每一行都是真实运行这段代码得到的结果。