为何Java多线程实现矩阵乘法性能反而低于单线程版本?
Java多线程矩阵乘法性能远低于单线程版本原因分析
问题背景
尝试通过多线程优化矩阵乘法代码性能时,添加线程后程序性能不升反降。额外尝试通过实现Runnable接口改写多线程逻辑后,性能下降幅度更大。
两个版本的实测耗时:
- 单线程实现:
0.0094- 手写多线程实现:
1.5917
目前测试中单线程版本是所有实现方案里性能最优的,当前正在研究CPU缓存对程序性能的影响。同逻辑的C语言多线程实现可以获得显著性能提升。
对应实现代码
单线程基准版本
public int[][] calculate(int[][] matriz1, int [][] matriz2, int matrixSize) { int[][] matrix = new int[matrixSize][matrixSize]; for(int i = 0; i < matrixSize; i++){ for(int k = 0; k < matrixSize; k++){ for(int j = 0; j < matrixSize; j++){ matrix[i][j] = matrix[i][j] + matriz1[i][k] * matriz2[k][j]; } } } return matrix; }
存在性能问题的多线程版本
public int[][] calculate(int[][] matriz1, int[][] matriz2, int matrixSize) { final int[][] matrix = new int[matrixSize][matrixSize]; CountDownLatch latchA = new CountDownLatch((int) (Math.pow(matrixSize, 3))); List<Thread> threads = new ArrayList<>(); for (int i = 0; i < matrixSize; i++) { finalI = i; Thread thread1 = new Thread(() -> { for (int k = 0; k < matrixSize; k++) { for (int j = 0; j < matrixSize; j++) { matrix[finalI][j] = matrix[finalI][j] + matriz1[finalI][k] * matriz2[k][j]; latchA.countDown(); } } }); thread1.start(); threads.add(thread1); if (threads.size() % 100 == 0) { waitForThreads(threads); } } try { latchA.await(); } catch (InterruptedException e) { e.printStackTrace(); } return matrix; } private void waitForThreads(List<Thread> threads) { for (Thread thread : threads) { try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } threads.clear(); }
性能暴跌的核心原因
- 线程生命周期开销完全覆盖并行收益:当前实现为矩阵每一行的计算单独创建一个原生Java线程,Java的
Thread和操作系统内核线程是1:1映射的,创建、调度、销毁线程需要内核参与,涉及栈内存分配、上下文切换、信号处理等固定开销,成本极高。从单线程仅耗时0.0094可以判断,测试用的矩阵规模很小,总计算量本身极低,这点计算量甚至抵不上创建上百个线程的开销。同时代码中每攒100个线程就调用join()阻塞等待,线程空转等待的时间占比极高,根本没有形成持续的并行计算流水。 - 热路径上的同步操作彻底摧毁缓存效率:代码将
latchA.countDown()放在了三层循环的最内层,总共要执行matrixSize^3次原子计数操作。CountDownLatch.countDown()底层靠CAS和内存屏障实现,每次调用都会强制跨核心同步缓存状态,导致CPU已经加载到L1/L2缓存的矩阵热点数据频繁失效,CPU无法做指令重排、缓存预取优化,内存访问延迟直接上涨几十上百倍,本来单线程下可以全程命中L1缓存的计算,变成了频繁访问主存的慢操作。 - 线程数量远超CPU物理核心承载上限:消费级CPU一般只有4-16个硬件线程,当前实现一次创建上百个线程,操作系统需要频繁在不同线程之间做上下文切换,每次切换都要把当前核心的缓存数据刷回主存、加载新线程的执行上下文,缓存命中率直接跌到谷底,多个核心同时争抢内存带宽,实际计算效率反而比单线程低很多。
- 和C语言多线程实现的逻辑差异:能获得性能提升的C语言多线程矩阵乘法实现,一般会创建和CPU核心数一致的固定工作线程,不会为每个子任务单独创建销毁线程,也不会在内层计算热路径加原子同步操作,同时会手动做内存对齐、缓存预取优化,且测试用的矩阵规模足够大,并行计算的收益可以覆盖线程调度开销。当前测试用例计算量太小,多线程调度的开销天然大于并行收益,单线程本来就是最优解。
正确优化方向
- 线程数严格匹配CPU可用硬件线程数,用固定大小的线程池做任务调度,不要为每一行计算单独创建线程,将矩阵按连续行块切分给固定数量的线程处理,避免频繁创建销毁线程的开销。
- 移除内层循环的
CountDownLatch计数逻辑,每个线程负责的整块计算任务完成后再做一次计数,总计数器数量和线程数保持一致即可,严禁在计算热路径放置任何原子操作、同步逻辑,避免破坏CPU缓存效率。 - 任务切分时保证每个线程访问的内存是连续块,避免跨线程共享缓存行,从内存布局上优化缓存命中率。
- 多线程性能测试要使用足够大的矩阵尺寸(如1024*1024以上),矩阵尺寸过小时总计算量太低,多线程的调度开销永远无法被并行收益覆盖。
内容的提问来源于stack exchange,提问作者Luiz Felipe Cruz Borges
相关产品推荐
相关产品推荐

