跳转至

Stream的并行流一定比串行流更快吗

⚡ Stream 的并行流一定比串行流快吗?

不,绝大多数情况下,默认使用并行流反而会让程序变慢甚至出错。 它是一把重型武器,只有在合适的战场(数据量大、计算密集、数据结构易于拆分)才能发挥威力。否则,线程调度、任务拆分、结果合并的开销会轻易吃掉并行带来的收益。


🧠 1. 并行流的底层:ForkJoinPool

并行流依靠 ForkJoinPool.commonPool()(一个 JVM 级别共享的线程池)来执行。它用 分治(Fork/Join) 策略,把一个大任务递归地拆分成若干小任务并行执行,最后再合并结果。

开销从哪来?

  • 拆分成本:不是所有数据源都能高效拆分(如 LinkedList 拆分很慢)。

  • 线程调度与上下文切换:任务太多时,线程调度器会频繁挂起和恢复线程,消耗 CPU。

  • 合并开销:并行执行完的小结果必须合并成最终结果,涉及数据复制和同步。

  • GC 压力:并行任务会产生更多临时对象。

如果任务本身很简单(如简单的加法、过滤),这些额外开销可能比串行执行的总时间还多。


📉 2. 何时并行流反而变慢?(几个典型反例)

反例 1:数据量太小

IntStream.range(1, 100).parallel().sum(); // 数据量小,拆分开销 > 计算开销

对于几百个元素的集合,串行流通常在微秒级完成,而并行流初始化线程池和拆分的成本就已经是毫秒级。

反例 2:数据结构拆分成本高

  • LinkedList:每次拆分都需要遍历到中间,O(n) 拆分成本。

  • HashSet/TreeSet:内部结构复杂,拆分需要额外计算或迭代。

  • Files.lines():读取文件本质是顺序的,强行并行收益极低。

反例 3:存在共享可变状态

List<Integer> result = new ArrayList<>();
list.parallelStream().forEach(result::add); // 线程不安全,结果错误或异常

ArrayList 不是线程安全的,多个线程并发 add 会导致数据覆盖、ArrayIndexOutOfBoundsException 等,同时内部的 Object[] 扩容会引发严重问题。

反例 4:计算简单但 IO 密集

并行流擅长 CPU 密集型任务。如果任务主要是 IO 等待(如远程调用、数据库查询),并行流只会增加线程数,让 IO 压力更大,而 CPU 大部分时间空闲。更合理的做法是用异步非阻塞或专用的 IO 线程池。


✅ 3. 何时适合用并行流?

  • 数据量超过万级以上(经验值),任务越重,拆分后的优势越明显。

  • 数据源是可高效拆分的:ArrayListint[]long[] 等数组或基于数组的结构,拆分成本接近 O(1)。

  • 操作是 CPU 密集且无状态的:复杂的数学计算、加密解密、图像处理等。

  • 操作无共享可变状态:如 reducecollect(Collectors.toList()) 等无副作用操作。

  • 避免在共享线程池中混入阻塞操作:不要用 commonPool 执行耗时 IO,应使用自定义 ForkJoinPool

实际建议: 使用 parallelStream() 前,务必用 JMH(Java Microbenchmark Harness) 在真实数据规模下对比串行和并行的吞吐量,并观察 CPU 利用率和 GC。一个简单的准则是:如果串行流已经能在几毫秒内完成,就不值得并行。


⚙️ 4. 如何安全地使用并行流?

  1. 只使用无状态、无副作用的函数,如 filtermapflatMapreduce 等。

  2. 避免 forEach 顺序依赖:forEach 是并行下的非确定性操作,如果必须保证顺序,用 forEachOrdered(但会降低并行度)。

  3. 使用并发安全的收集器:Collectors.toList() 是线程安全的,但自定义收集器需注意。

  4. 限制并行度:可在启动时通过 -Djava.util.concurrent.ForkJoinPool.common.parallelism=N 限制公共池线程数,防止过度占用 CPU。

  5. 大数据量下用自定义 ForkJoinPool 隔离,避免影响系统中其他依赖公共池的代码(如 CompletableFuture 默认回调)。


📊 5. 对比总结

查看内嵌表格


一句话总结:并行流不是免费的午餐,它是用“计算资源换时间”的加速器,但开关一开,拆、调、合的开销随之而来。只有在数据量大、计算重、数据源友好的情况下,它才跑得比串行快。