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系统上运行该程序。
问题原因与优化方案
核心原因
- 任务粒度太小:给每个(row, col)位置单独创建任务提交到线程池,单个
addChildrenForPosition的计算量远小于线程调度、任务提交的开销,线程池的调度成本直接抵消甚至超过了并行计算的收益。 - 线程安全问题:
HashSet并非线程安全集合,多线程同时写入时会触发并发修改异常或数据不一致,隐性的修复、重复元素处理会额外消耗资源,甚至影响minimax搜索的正确性,导致搜索深度下降。 - 线程数设置不合理:如果你的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.
相关产品推荐
相关产品推荐

