你以为 `parallel()` 一加就更快?那为什么很多时候它反而更慢?

张开发
2026/4/3 3:44:17 15 分钟阅读
你以为 `parallel()` 一加就更快?那为什么很多时候它反而更慢?
你好欢迎来到我的博客我是【菜鸟不学编程】我是一个正在奋斗中的职场码农步入职场多年正在从“小码农”慢慢成长为有深度、有思考的技术人。在这条不断进阶的路上我决定记录下自己的学习与成长过程也希望通过博客结识更多志同道合的朋友。️ 主要方向包括 Java 基础、Spring 全家桶、数据库优化、项目实战等也会分享一些踩坑经历与面试复盘希望能为还在迷茫中的你提供一些参考。 我相信写作是一种思考的过程分享是一种进步的方式。如果你和我一样热爱技术、热爱成长欢迎关注我一起交流进步全文目录前言I. 并行 Stream 基础parallel() 和 ForkJoinPool1两种写法parallel() vs parallelStream()2默认线程池Common ForkJoinPool别忽视它3一个最小示例CPU 密集型才谈并行II. 任务拆分Spliterator 和特性标志1Spliterator 是啥一句话2特性标志Characteristics决定“优化上限”3为什么 ArrayList 经常比 LinkedList 更适合并行III. 性能陷阱stateful 操作和共享变量1Stateful 中间操作一不小心就把并行变串行2共享变量并行流里最常见的“隐形炸弹”3千万别在并行流里干阻塞 IOIV. 自定义收集器Collector.of 方法1Collector.of 的三件事2示例高性能统计min/max/sum/count一次搞定3什么时候需要 CONCURRENTV. 基准测试JMH 工具的使用1一个最小 JMH 基准比较 stream vs parallelStream2JMH 的几个“别犯傻提示”VI. 应用大数据处理的并行计算何时用、怎么用才不翻车1这是 CPU 密集型吗2数据源容易拆分吗3流水线是不是“可并行友好”一个更“像工程”的并行计算示例分桶聚合避免共享变量收尾并行 Stream 的正确姿势带点情绪但不站队 写在最后前言先确认一下你这里的“大数据处理”是指内存里几十万/几百万条集合处理还是指IO/数据库/文件流式处理我下面先按“CPU 密集型集合计算”来写这是并行 Stream 最适合的场景IO 场景我会专门提醒哪些千万别并行。I. 并行 Stream 基础parallel()和ForkJoinPool并行 Stream 的本质其实很简单把一条流水线拆成多个子任务丢进 ForkJoinPool然后合并结果。1两种写法parallel()vsparallelStream()list.stream().parallel().map(...).reduce(...);list.parallelStream().map(...).reduce(...);效果差不多主要区别是表达方式。工程里我更喜欢stream().parallel()因为它更容易在链路里“看清楚哪里开始并行”。2默认线程池Common ForkJoinPool别忽视它并行 Stream 默认用的是ForkJoinPool.commonPool()。这意味着你的并行 Stream 会和别的并行任务CompletableFuture、其他并行 Stream抢线程公共池的并行度通常与 CPU 核数相关不是越多越好如果你在公共池里干了阻塞 IO —— 恭喜你把大家一起拖下水3一个最小示例CPU 密集型才谈并行importjava.util.*;importjava.util.stream.*;publicclassParallelBasics{publicstaticvoidmain(String[]args){ListIntegernumsIntStream.range(0,1_000_000).boxed().toList();longsumnums.parallelStream().mapToLong(x-(long)x*x).sum();System.out.println(sum);}}这类纯计算通常能看到收益。但如果你在 map 里做网络请求、读文件、打日志……那就别急着并行我们后面会骂温柔地骂。II. 任务拆分Spliterator 和特性标志并行 Stream 能不能跑得快很多时候取决于一句话能不能“拆得均匀”以及拆起来“值不值”。而拆分这件事幕后功臣就是Spliterator。1Spliterator 是啥一句话它是集合/数据源提供给 Stream 的一个“可拆分迭代器”。并行时框架会不断调用trySplit()把任务切成更小块丢给不同线程。2特性标志Characteristics决定“优化上限”常见几个标志你至少要认识它们的“人话含义”SIZED我知道大小能更好地分配任务/数组预分配SUBSIZED拆分后的子迭代器也知道大小更利于均匀拆分ORDERED有顺序并行合并时要维护顺序成本更高SORTED已排序某些操作可以利用DISTINCT无重复IMMUTABLE/CONCURRENT并发安全/不可变减少防御成本3为什么 ArrayList 经常比 LinkedList 更适合并行因为ArrayList的 spliterator 更容易等分且SIZED/SUBSIZED明确LinkedList拆分要沿链走切分成本高、还不均匀。所以很多时候你看到“并行更慢”根本原因可能是**你拿着一把钝刀数据结构去切牛排任务拆分。**III. 性能陷阱stateful 操作和共享变量这一节我得稍微严肃一点并行 Stream 的坑80% 都在这里。1Stateful 中间操作一不小心就把并行变串行典型的 stateful 中间操作包括distinct()sorted()limit()/skip()尤其在有序流上某些groupingBy的组合如果收集器/合并不高效这些操作需要维护全局状态或顺序并行时会引入同步/缓冲/合并开销。并行度上去了开销也上去了最后反而慢。举个例子varrlist.parallelStream().sorted().limit(1000).toList();你想要的是“快”但你给它加了“必须全局排序 保持顺序 截断”——这就像让八个人抬一张桌子还要求每一步都走得一模一样齐。能不慢吗2共享变量并行流里最常见的“隐形炸弹”最经典的错误写法importjava.util.concurrent.atomic.AtomicLong;AtomicLongsumnewAtomicLong();list.parallelStream().forEach(x-sum.addAndGet(x));你以为线程安全了是的安全了但慢了每次 add 都要做原子操作争用。并行的意义被锁/原子争用吃掉了。正确姿势用归约reduce或内建收集器longsumlist.parallelStream().mapToLong(Long::valueOf).sum();3千万别在并行流里干阻塞 IO比如在 map 里调 HTTP、查 DB、读磁盘list.parallelStream().map(x-httpCall(x)).toList();这会把 commonPool 线程都卡住。并行流不是给 IO 设计的。IO 场景更适合异步 IO、专用线程池、批处理策略或者干脆别并行。IV. 自定义收集器Collector.of方法这部分特别关键因为很多并行 Stream 的性能最后都卡在“怎么收集/怎么合并”。1Collector.of 的三件事Collector.of(supplier, accumulator, combiner, characteristics...)supplier创建容器accumulator把元素塞进容器combiner并行时把多个容器合并characteristics告诉框架是否并发安全、是否无序等2示例高性能统计min/max/sum/count一次搞定我们写个Stats容器publicclassStats{longcount;longsum;longminLong.MAX_VALUE;longmaxLong.MIN_VALUE;voidaccept(longx){count;sumx;minMath.min(min,x);maxMath.max(max,x);}Statscombine(Statsother){StatssnewStats();s.countthis.countother.count;s.sumthis.sumother.sum;s.minMath.min(this.min,other.min);s.maxMath.max(this.max,other.max);returns;}doubleavg(){returncount0?0:(double)sum/count;}}收集器importjava.util.stream.Collector;publicclassStatsCollector{publicstaticCollectorLong,Stats,StatsstatsCollector(){returnCollector.of(Stats::new,(st,x)-st.accept(x),Stats::combine);}}使用Statsstlist.parallelStream().map(Long::valueOf).collect(StatsCollector.statsCollector());System.out.println(countst.count, sumst.sum, minst.min, maxst.max, avgst.avg());这里的关键在 combiner合并要快且尽量少分配。上面combine返回新对象是演示写法追求性能时可以让combine在this上就地合并减少对象创建。3什么时候需要 CONCURRENT如果你的容器支持并发累积比如ConcurrentHashMap并且你不关心顺序可以加Collector.Characteristics.CONCURRENT, UNORDERED让并行时减少合并成本。但别为了加而加加错了会出错数据丢/竞态那就尴尬了。V. 基准测试JMH 工具的使用并行优化最容易犯的罪叫我感觉更快。感觉这玩意儿在性能领域基本等于“幻觉”。所以 JMH 必须上。1一个最小 JMH 基准比较 stream vs parallelStreamimportorg.openjdk.jmh.annotations.*;importjava.util.concurrent.TimeUnit;importjava.util.stream.IntStream;importjava.util.*;BenchmarkMode(Mode.Throughput)OutputTimeUnit(TimeUnit.SECONDS)Warmup(iterations3,time1)Measurement(iterations5,time1)Fork(1)State(Scope.Thread)publicclassParallelStreamBench{privateListIntegerdata;Setuppublicvoidsetup(){dataIntStream.range(0,5_000_000).boxed().toList();}BenchmarkpubliclongsequentialSumSquares(){returndata.stream().mapToLong(x-(long)x*x).sum();}BenchmarkpubliclongparallelSumSquares(){returndata.parallelStream().mapToLong(x-(long)x*x).sum();}}2JMH 的几个“别犯傻提示”先 warmup别拿冷启动数据当结论别在 benchmark 里 printlnI/O 会毁掉结果注意 GC对象分配多会影响吞吐Fork至少 1 次隔离 JVM 状态更严谨可多 fork我见过有人跑一次 JMH 就下结论跟掷骰子差不多……别这样真的。VI. 应用大数据处理的并行计算何时用、怎么用才不翻车你要把并行 Stream 用在“大数据处理”先问自己三个问题非常务实1这是 CPU 密集型吗✅ 数值计算、压缩/哈希、复杂解析、图像处理、规则匹配 → 适合并行❌ 网络请求、磁盘 IO、DB 查询 → 不适合直接 parallelStream2数据源容易拆分吗✅ ArrayList、数组、范围 IntStream → 拆得好❌ LinkedList、Iterator 风格、生成器式流 → 拆分差收益小3流水线是不是“可并行友好”✅ map/filter/reduce 这种无状态操作 → 友好❌ sorted/distinct/limit 有序约束 → 成本高谨慎一个更“像工程”的并行计算示例分桶聚合避免共享变量场景对大量整数做分桶统计比如按区间统计数量。错误示范共享 HashMap 锁/原子争用先不写了写了我怕你气。我们用groupingByConcurrent并发收集器更合理importjava.util.concurrent.ConcurrentMap;importjava.util.stream.IntStream;importjava.util.*;importstaticjava.util.stream.Collectors.*;publicclassBucketCount{publicstaticvoidmain(String[]args){ListIntegerdataIntStream.range(0,10_000_000).boxed().toList();ConcurrentMapInteger,Longbucketsdata.parallelStream().collect(groupingByConcurrent(x-x/1000,// 每 1000 一个桶counting()));System.out.println(bucketsbuckets.size());System.out.println(bucket[123]buckets.get(123));}}这类“并发友好收集器”能显著减少你手写锁和原子争用的痛苦。让框架干框架擅长的事你别自己造个半吊子并发容器。收尾并行 Stream 的正确姿势带点情绪但不站队我对并行 Stream 的态度一直挺中立它不是银弹但也不是洪水猛兽。它更像一个“高压锅”你会用就能省时间、提吞吐你乱用就能把厨房炸得很热闹最后给你一句我自己用来判断要不要.parallel()的反问挺好用的这段流水线如果我把它拆成 8 份并行跑合并结果的成本会不会比计算本身还高如果答案是“会”那就别 parallel。如果答案是“不会”那就上但请你务必用 JMH 验证——别靠感觉。 写在最后如果你觉得这篇文章对你有帮助或者有任何想法、建议欢迎在评论区留言交流你的每一个点赞 、收藏 ⭐、关注 ❤️都是我持续更新的最大动力我是一个在代码世界里不断摸索的小码农愿我们都能在成长的路上越走越远越学越强感谢你的阅读我们下篇文章再见✍️ 作者某个被流“治愈”过的 Java 老兵 日期2026-01-07 本文原创转载请注明出处。

更多文章