Java多线程并行程序耗时接近串行的原因排查
Why does multi-threaded CPU-bound code perform worse than single-threaded on Apple M1?
我来帮你拆解这个反直觉的问题——你遇到的性能瓶颈根本不是CPU调度的问题,而是你的computation()函数里隐藏了大量内存分配与垃圾回收(GC)开销,多线程场景下这些开销被急剧放大,直接拖慢了整体性能。
问题出在你的computation()函数里
先仔细看你的计算逻辑:
private static void computation() { long n = (long) Math.pow(10, 7); var sum = 0; for (long i = 0; i < n; i++) { sum += LongStream.range(1, 9).boxed().limit(n).map(l -> new BigDecimal(String.valueOf(l))).distinct().count(); } }
这里有两个致命的冗余点:
- 疯狂的临时对象创建:每次循环都会生成8个
BigDecimal实例,1亿次循环下来就是8亿个临时对象——这会让JVM的垃圾回收器彻底忙不过来。 - 完全多余的Stream操作:
LongStream.range(1,9)本来就只有8个不重复的元素,limit(n)和distinct()不会改变最终count()的结果(始终是8),这些操作纯粹是在浪费资源。
单线程时,GC的压力还能勉强应付;但8个线程同时疯狂分配对象,会引发连锁反应:
- Young GC的触发频率呈指数级上升
- GC的停顿时间(尤其是STW阶段)会让所有线程等待,大量CPU时间被浪费在内存回收上,而非真正的计算
- 多线程下的内存分配还会带来额外的同步开销,进一步拖慢速度
这就是为什么你看到CPU使用率高达790%,但耗时却接近单线程的8倍——大部分CPU时间都花在处理GC和内存同步上了。
验证猜想的简单方法
你可以做两个测试来确认这个结论:
- 替换无意义的计算逻辑:把
computation()里的Stream操作直接换成常量8,比如:
private static void computation() { long n = (long) Math.pow(10, 7); var sum = 0; for (long i = 0; i < n; i++) { sum += 8; // 直接返回固定值 } }
再跑8线程测试,你会发现耗时和单线程几乎一致——因为此时没有了对象分配和GC开销,真正的CPU密集型任务可以充分利用多核心。
- 查看GC日志:添加JVM参数
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps,对比单线程和8线程的GC情况。你会发现8线程时GC的总停顿时间占比极高,这就是耗时增加的核心原因。
修复方案
如果这是模拟代码,调整计算逻辑即可;如果是实际业务场景,你需要:
- 减少循环内的对象分配:尽量重用对象(比如用对象池)、使用基本类型代替包装类型,避免在高频循环里创建临时对象。
- 选择合适的GC:对于多线程下的大内存分配场景,Java 11+的ZGC或者Shenandoah GC是更好的选择,它们的停顿时间更短,对多线程性能影响更小。
- 优化计算逻辑:像你代码里的冗余Stream操作,一定要提前剔除,避免做无意义的工作。
另外你提到绑定核心没有改善,这很正常——因为问题根本不在CPU核心的调度,而是内存和GC的瓶颈,所以绑定核心解决不了本质问题。
内容的提问来源于stack exchange,提问作者NicuMarasoiu
相关产品推荐
相关产品推荐

