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 密集型计算 | ✅ 并行可能提升性能 |
📌 提醒语句:
并行流不是魔法,它能快,是因为你不给它添乱。如果你非要访问外部变量、共享集合、做顺序相关的操作,它不但不会快,反而容易炸了锅!