forEach 和 forEachOrdered 在并行流中的表现有什么不同?
在 Java 的并行流(Parallel Stream)中,forEach 和 forEachOrdered 的核心区别在于是否保证按照数据源的“遭遇顺序”(Encounter Order)来执行操作。
这不仅影响输出的顺序,还直接关系到执行性能。
以下是它们在并行流中的详细对比和底层差异:
1. 核心区别对比
| 特性 | forEach |
forEachOrdered |
|---|---|---|
| 顺序保证 | 不保证。元素被多线程并发处理,先处理完的先执行操作,顺序是随机的。 | 保证。严格按照数据源的原始顺序(如 List 的索引顺序)执行操作。 |
| 并行性能 | 极高。发挥并行流的最大优势,线程之间无需同步等待顺序。 | 较低。为了保证顺序,底层需要进行同步和缓存,会抵消部分并行带来的性能提升。 |
| 适用场景 | 元素的执行顺序无关紧要(如:累加到并发容器、打印无序日志)。 | 必须按顺序处理(如:按顺序写入文件、需要依赖前一个元素计算结果)。 |
2. 代码示例与输出
我们用一个简单的代码来观察它们在并行流中的表现:
java
import java.util.Arrays;
import java.util.List;
public class StreamTest {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
System.out.println("--- 使用 forEach (并行流) ---");
list.parallelStream().forEach(x -> System.out.print(x + " "));
System.out.println("\n\n--- 使用 forEachOrdered (并行流) ---");
list.parallelStream().forEachOrdered(x -> System.out.print(x + " "));
}
}
运行结果(可能的一次输出):
plaintext
--- 使用 forEach (并行流) ---
6 5 8 9 7 3 4 1 2 <-- 每次运行结果都可能不同,顺序完全随机
--- 使用 forEachOrdered (并行流) ---
1 2 3 4 5 6 7 8 9 <-- 无论运行多少次,顺序永远是 1 到 9
3. 底层原理剖析:它们在并行流中是如何工作的?
(1) forEach 的行为
在并行流中,数据被拆分(Fork)成多个子任务,分发给 ForkJoinPool 中的多个工作线程。
当某个线程完成了对某个元素(或子块)的处理,它会立即在这个线程中执行 forEach 传入的 Consumer。因为各线程执行速度不同,所以最终的执行顺序是随机、非确定性的。
- 优点:没有线程等待,吞吐量最大。
(2) forEachOrdered 的行为
即便在并行流中,forEachOrdered 也会确保最终的 Action 按照流的遭遇顺序执行。
- 它是如何做到的?
各个线程仍然会并行地处理中间操作(例如map,filter等),但到了最后的forEachOrdered步骤时,系统会引入一个协调机制(类似于锁或缓冲区)。
即使线程 B 先处理完了后面的元素,它也不能立即执行 Action,必须等待处理前面元素的线程 A 完成并消费后,才能轮到它。 - 代价:这种顺序协调机制需要额外的开销,会导致线程阻塞和等待,从而大大降低并行效率。
4. 性能与最佳实践建议
无序源 vs 有序源:
如果数据源本身是无序的(例如HashSet),那么forEachOrdered和forEach的表现差异不大。但如果是ArrayList、数组等有序源,forEachOrdered会引入明显的性能开销。何时使用
forEach:- 只要不关心处理顺序,强烈推荐在并行流中使用
forEach,这样才能充分发挥多核 CPU 的威力。 - 例如:将计算结果放入一个线程安全的并发容器中(如
ConcurrentHashMap)。
- 只要不关心处理顺序,强烈推荐在并行流中使用
何时使用
forEachOrdered:- 只有在必须保证顺序,同时中间操作极其耗时(适合并行),最后一步又必须按顺序输出时,才在并行流中使用
forEachOrdered。 - 警告:如果中间操作很简单(例如只是简单的数值计算),而你又需要有序输出,直接使用串行流(Sequential Stream)+
forEach通常比 并行流 +forEachOrdered更快。
- 只有在必须保证顺序,同时中间操作极其耗时(适合并行),最后一步又必须按顺序输出时,才在并行流中使用
右滑查看面试常问