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

Java线程池版本比串行版性能更差的原因及优化咨询

并行化节点扩展任务后性能不升反降的问题

我有一个用于扩展给定节点以查找后续可能节点(children)并将其保存/返回至childrenSet的程序。最初采用串行实现,代码如下:

private Set<ReversiState> getChildrenSerial() {

    HashSet<ReversiState> childrenSet = new HashSet<>();

    // For each piece of the player, add the children.
    for(int row=0; row<BOARD_SIZE; row++){
        for(int col=0; col<BOARD_SIZE; col++){

            // This is where the heavy work happens
            addChildrenForPosition(childrenSet, row, col);
        }
    }

    return childrenSet;
}

串行版本耗时约9.7秒,调用此方法的minimax算法平均可搜索至7.0层节点。

为实现更深层次的搜索,我尝试使用静态final修饰的Java ThreadPoolExecutor,代码如下:

private static final int NB_THREADS = 8;
private static final ThreadPoolExecutor executor = (ThreadPoolExecutor) 
    Executors.newFixedThreadPool(NB_THREADS);

并实现了getChildrenParallel方法,逻辑与串行版本基本一致,但将addChildrenForPosition任务提交至线程池处理:

private Set<ReversiState> getChildrenParallel() {

    HashSet<Future<Void>> threadResults = new HashSet<>();
    HashSet<ReversiState> childrenSet = new HashSet<>();

    // For each piece of the player, add the children.
    for(int row=0; row<BOARD_SIZE; row++){
        for(int col=0; col<BOARD_SIZE; col++){

            // Multi-threading takes final variables.
            final Integer rowFinal = row;
            final Integer colFinal = col;

            // Submit a task to the thread pool.
            Future<Void> future = executor.submit(

                     // This is the method where the heavy work happens
                () -> addChildrenForPosition(childrenSet, rowFinal, colFinal), 
                null);
            threadResults.add(future);
        }
    }

    // Wait for all tasks to finish.
    for(Future<Void> future : threadResults){
        try{
            future.get();
        } catch(Exception e){
            e.printStackTrace();
        }
    }
    return childrenSet;
}

原本预期并行版本会比串行版更快,但实际平均耗时11秒,minimax算法的平均搜索深度降至6.3层,性能反而略逊于串行实现。

请问这是什么原因导致的?是线程池提交任务的开销过大?还是单个任务粒度太小?我该如何优化?

注:我在Windows 11系统上运行该程序。


问题原因与优化方案

核心原因

  1. 任务粒度太小:给每个(row, col)位置单独创建任务提交到线程池,单个addChildrenForPosition的计算量远小于线程调度、任务提交的开销,线程池的调度成本直接抵消甚至超过了并行计算的收益。
  2. 线程安全问题:HashSet并非线程安全集合,多线程同时写入时会触发并发修改异常或数据不一致,隐性的修复、重复元素处理会额外消耗资源,甚至影响minimax搜索的正确性,导致搜索深度下降。
  3. 线程数设置不合理:如果你的CPU核心数不足8个(比如常见的4核8线程笔记本),8个线程会引发频繁的线程切换,进一步增加调度成本。

优化方案

1. 增大任务粒度

不要给每个(row, col)单独创建任务,而是按行或块划分任务,让单个任务处理多行数据,减少任务提交次数与调度开销,同时每个线程操作本地集合避免并发冲突:

private Set<ReversiState> getChildrenParallel() {
    List<Future<Set<ReversiState>>> futures = new ArrayList<>();
    int rowsPerTask = BOARD_SIZE / NB_THREADS;
    if (rowsPerTask == 0) rowsPerTask = 1;

    for (int startRow = 0; startRow < BOARD_SIZE; startRow += rowsPerTask) {
        int endRow = Math.min(startRow + rowsPerTask, BOARD_SIZE);
        final int finalStartRow = startRow;
        final int finalEndRow = endRow;

        futures.add(executor.submit(() -> {
            HashSet<ReversiState> localSet = new HashSet<>();
            for (int row = finalStartRow; row < finalEndRow; row++) {
                for (int col = 0; col < BOARD_SIZE; col++) {
                    addChildrenForPosition(localSet, row, col);
                }
            }
            return localSet;
        }));
    }

    HashSet<ReversiState> childrenSet = new HashSet<>();
    for (Future<Set<ReversiState>> future : futures) {
        try {
            childrenSet.addAll(future.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    return childrenSet;
}

2. 合理设置线程池大小

对于CPU密集型任务,线程数最优值通常为CPU核心数 + 1,可通过系统API动态获取:

private static final int NB_THREADS = Runtime.getRuntime().availableProcessors() + 1;
private static final ThreadPoolExecutor executor = (ThreadPoolExecutor) 
    Executors.newFixedThreadPool(NB_THREADS);

3. 使用并行流简化实现

Java 8+的并行流可自动处理任务划分与线程调度,代码更简洁且能自适应系统核心数:

private Set<ReversiState> getChildrenParallel() {
    return IntStream.range(0, BOARD_SIZE)
            .boxed()
            .parallel()
            .flatMap(row -> {
                HashSet<ReversiState> localSet = new HashSet<>();
                for (int col = 0; col < BOARD_SIZE; col++) {
                    addChildrenForPosition(localSet, row, col);
                }
                return localSet.stream();
            })
            .collect(Collectors.toSet());
}

4. 避免共享集合(可选)

如果必须使用共享集合,替换为线程安全的实现,比如ConcurrentHashMap.newKeySet(),但写入开销较高,优先推荐“本地集合+结果合并”的方案。

内容的提问来源于stack exchange,提问作者Enes K.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 15:35:24