339. Java Stream API - 并行流中的副作用陷阱与顺序敏感操作

0 阅读3分钟

339. Java Stream API - 并行流中的副作用陷阱与顺序敏感操作


🎯 并行流看起来很美,但背后暗藏陷阱!

在使用 parallelStream() 时,我们希望的是:

  • 🚀 利用多核 CPU 提升性能
  • 🧩 自动并行处理每个元素

但现实中,如果你不小心写错了代码,结果可能是:

  • ❌ 错误的输出
  • 💥 异常崩溃
  • 😵 性能反而变差

让我们来逐个拆解并行流的问题根源。


🧱 处理子流与处理完整流有何不同?

在并行流中,每一小块数据被拆成子任务(sub-stream),并由不同线程处理。但如果子任务之间共享状态或依赖顺序,就会出问题!


🧨 问题一:访问外部状态(副作用)

🤔 什么是“外部状态”?

外部状态 = 不属于流本身、但在流处理过程中被读写的变量或对象。 比如:

List<Integer> results = new ArrayList<>();  // ❗外部状态

IntStream.range(0, 1000)
         .parallel()
         .forEach(results::add);  // 💥 多线程同时 add

🧪 实验:并发写入 ArrayList

List<Integer> ints = new ArrayList<>();

IntStream.range(0, 1_000_000)
         .parallel()
         .forEach(ints::add);  // ❗ 非线程安全操作!

System.out.println("ints.size() = " + ints.size());
📌 实际输出(常见结果):
ints.size() = 387122

甚至可能抛出:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException

🚨 原因分析:

  • ArrayList 不是线程安全结构
  • 多个线程并发写入,导致数据覆盖、丢失或索引错乱
  • 并行流默认使用 ForkJoinPool.commonPool,每个线程并发访问共享资源 ➡️ 出事了

✅ 正确做法:使用线程安全结构或无副作用方式

List<Integer> safe = IntStream.range(0, 1_000_000)
                              .parallel()
                              .boxed()
                              .collect(Collectors.toList());  // ✅ 无副作用方式

或者使用线程安全容器:

List<Integer> sync = Collections.synchronizedList(new ArrayList<>());

IntStream.range(0, 1_000_000)
         .parallel()
         .forEach(sync::add);  // ✅ 但性能差,得不偿失

🧠 建议:并行流中不要修改外部变量!


🔄 问题二:顺序敏感的操作(stateful operations)

有些 Stream 操作必须“记住顺序”才能正确运行,比如:

操作含义
limit(n)只取前 n 个元素
skip(n)跳过前 n 个元素
findFirst()查找第一个元素

这些操作被称为:

Stateful Operations(有状态操作)

因为它们内部需要维护状态(例如计数器)。


🧪 示例:并行使用 limit()

List<Integer> list = IntStream.range(0, 1000).boxed().toList();

List<Integer> firstTen = list.parallelStream()
                             .limit(10)
                             .toList();

System.out.println(firstTen);
🚨 并行运行时可能输出乱序结果!
[212, 103, 7, 56, 918, 0, 402, 321, 15, 68]  // ❗数据对了,但顺序错了!

🧠 原因:

  • 多线程并发处理元素
  • limit(10) 必须跨线程维护一个共享计数器
  • 高开销 + 非确定性顺序

✅ 正确做法:如果你需要顺序,请使用 .stream().forEachOrdered()

list.parallelStream()
    .limit(10)
    .forEachOrdered(System.out::println);  // ✅ 保证顺序但牺牲并行性能

🧠 小知识:谁会引起顺序/副作用问题?

操作并行友好?是否依赖顺序?是否可能副作用?
map()✅ 是❌ 否❌ 否
forEach()✅ 是❌ 否✅ 是(取决于你写的代码)
forEachOrdered()❌ 否✅ 是✅ 是
limit()❌ 否✅ 是❌ 否
collect()✅ 是(如果 collector 是并发安全的)❌ 否❌ 否
add() 到外部 List❌ 否❌ 否✅ 是

✅ 总结:并行流使用指南

使用情景是否适合并行流?
无副作用、无顺序依赖的 map/filter✅ 非常适合
修改共享集合或外部变量❌ 禁止
顺序敏感操作(limit/findFirst/skip)⚠️ 谨慎使用
少量数据(< 1,000)❌ 串行更快
大数据、CPU 密集型计算✅ 并行可能提升性能

📌 提醒语句:

并行流不是魔法,它能快,是因为你不给它添乱。如果你非要访问外部变量、共享集合、做顺序相关的操作,它不但不会快,反而容易炸了锅