You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

二维数组求和:ForkJoinPool比单线程运行更慢的原因排查

为什么ForkJoinPool求和二维数组比单线程还慢?

我尝试用ForkJoinPool对二维数组求和,结果运行速度反而比单线程实现慢。测试arr[15][10]时,单线程耗时402200纳秒,ForkJoin耗时3241900纳秒,完全搞不懂原因。

单线程实现代码

private static void calc(int[][] arr) {
    int sum = 0;
    int row = arr.length;
    int column = arr[0].length;
    
    for (int i = 0; i < row; i++) {
        for (int j = 0; j < column; j++) {
            sum += arr[i][j];
        }
    }
    System.out.print("sum: " + sum + "\n");
}

性能测试代码

long startTime = System.nanoTime();
calc(arr);
long stopTime = System.nanoTime();
System.out.println("one thread: " + (stopTime - startTime) + "\n");

ForkJoin实现代码

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;

class ForkJoin extends RecursiveAction {
  int maxCall;
  int[] data;
  int start, end;
  public static int sum = 0;
  
  ForkJoin(int[] nums, int s, int e, int t) {
    data = nums;
    start = s;
    end = e;
    maxCall = t;
  }

  protected void compute() {
    if ((end - start) < maxCall) {
      for (int i = start; i < end; i++) {
          sum += data[i];
      }
    } else {
      int middle = (start + end) / 2;
      invokeAll(new ForkJoin(data, start, middle, maxCall), new ForkJoin(data, middle, end, maxCall));
    }
  }

    
  public ForkJoin(int[][] nums) {
        int threads = 32;
        int maxCall = 10000;
        long beginT, endT;

        ForkJoinPool fjp = new ForkJoinPool(threads);

        int row = nums.length;
        int column = nums[0].length;
        for (int i = 0; i < row; i++) {
            ForkJoin task = new ForkJoin(nums[i], 0, column, maxCall);
            fjp.invoke(task);
        }
        System.out.println("sum: " + sum);
  }
}

核心原因分析

  • 任务粒度太小,线程开销远超计算收益
    你的测试数组总共只有150个元素,这点计算量单线程瞬间就能完成。但ForkJoin的任务拆分、线程调度、上下文切换都有额外开销,这些开销对于这么小的计算量来说,完全盖过了多线程并行的收益。哪怕你设置的maxCall=10000远大于单行列数10,每个行任务根本不会拆分,循环提交15个小任务到线程池依然会产生调度成本。

  • 错误的任务提交方式导致串行执行
    在ForkJoin的构造方法里,你循环调用fjp.invoke(task)——这个方法会阻塞当前线程直到任务完成,相当于你拿着多线程池却在做单线程的事,平白多了线程调度的成本,完全没用到并行。

  • 线程池设置过大引发上下文切换
    你创建了32线程的ForkJoinPool,但普通CPU核心数一般在4-16之间,过多的线程会导致频繁的上下文切换,进一步增加不必要的开销。

  • 共享静态变量的潜在开销
    你用public static int sum作为累加变量,虽然当前任务是串行提交的,但静态变量的访问可能存在隐含的同步开销,而且如果真的并行执行还会有线程安全问题。


优化建议

  1. 只在大数据量时使用并行
    ForkJoin的优势只在数据量足够大(比如百万级以上元素)时才会体现,小数据量直接用单线程更高效。

  2. 正确提交并行任务
    不要循环串行调用invoke,应该把整个二维数组拆分成大的并行任务,或者将所有行任务一次性提交到线程池,用invokeAll批量处理后等待结果。

  3. 合理设置线程池大小
    直接用new ForkJoinPool()即可,它默认会根据CPU核心数设置线程数,手动设置过大的线程数只会增加切换开销。

  4. 用任务返回结果替代共享变量
    改用RecursiveTask<Integer>代替RecursiveAction,让每个子任务返回自己的求和结果,最后在父任务中合并结果,既避免线程安全问题,又消除共享变量的开销。

优化后的ForkJoin示例代码

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

class ArraySumTask extends RecursiveTask<Integer> {
    private final int[][] arr;
    private final int startRow;
    private final int endRow;
    // 阈值根据实际数据量调整,数据越大阈值可以设越高
    private static final int THRESHOLD = 100;

    public ArraySumTask(int[][] arr, int startRow, int endRow) {
        this.arr = arr;
        this.startRow = startRow;
        this.endRow = endRow;
    }

    @Override
    protected Integer compute() {
        int rowCount = endRow - startRow;
        // 小粒度任务直接单线程计算
        if (rowCount <= THRESHOLD) {
            int sum = 0;
            for (int i = startRow; i < endRow; i++) {
                for (int num : arr[i]) {
                    sum += num;
                }
            }
            return sum;
        }
        // 大粒度任务拆分为两个子任务并行执行
        int midRow = startRow + rowCount / 2;
        ArraySumTask leftTask = new ArraySumTask(arr, startRow, midRow);
        ArraySumTask rightTask = new ArraySumTask(arr, midRow, endRow);
        
        leftTask.fork(); // 异步执行左任务
        int rightSum = rightTask.compute(); // 同步执行右任务
        int leftSum = leftTask.join(); // 等待左任务结果
        
        return leftSum + rightSum;
    }

    public static void main(String[] args) {
        // 用大数据量测试才能体现并行优势
        int[][] arr = new int[10000][1000];
        // 初始化数组(示例:填充随机数)
        for (int i = 0; i < arr.length; i++) {
            for (int j = 0; j < arr[i].length; j++) {
                arr[i][j] = (int) (Math.random() * 100);
            }
        }

        ForkJoinPool pool = new ForkJoinPool();
        long startTime = System.nanoTime();
        int totalSum = pool.invoke(new ArraySumTask(arr, 0, arr.length));
        long stopTime = System.nanoTime();
        
        System.out.println("sum: " + totalSum);
        System.out.println("ForkJoin耗时: " + (stopTime - startTime) + "纳秒");
    }
}

内容的提问来源于stack exchange,提问作者Lex

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 22:13:21