文章目录
- [295. Java Stream API - 选择适用于并行计算的 BinaryOperator](#295. Java Stream API - 选择适用于并行计算的 BinaryOperator)
-
-
- [🚧 什么是并行计算下的归约?](#🚧 什么是并行计算下的归约?)
- [📦 模拟并行归约的过程(简化版)](#📦 模拟并行归约的过程(简化版))
- [🔍 归约顺序的不同拆法(Associativity)](#🔍 归约顺序的不同拆法(Associativity))
- [🎓 什么是结合性(Associativity)?](#🎓 什么是结合性(Associativity)?)
- [⚠️ 为什么结合性重要?](#⚠️ 为什么结合性重要?)
- [✅ 如何确保 BinaryOperator 是安全的?](#✅ 如何确保 BinaryOperator 是安全的?)
- [📌 小结表格](#📌 小结表格)
-
- [💬 总结](#💬 总结)
-
295. Java Stream API - 选择适用于并行计算的 BinaryOperator
在使用 Java Stream API 进行归约(reduce())时,我们可以利用 并行流(parallel streams) 来提升性能。但这里有一个非常重要的前提条件 :你传入的 BinaryOperator 必须满足一个数学属性 ------ 结合性(Associativity)。
🚧 什么是并行计算下的归约?
Java 的 Stream API 支持并行处理,方式很简单,只需要调用:
java
stream.parallel()
但背后发生了什么呢?
- Java 会把源数据拆分成多个部分
- 每部分分别用相同的 BinaryOperator 做归约
- 然后将各部分的中间结果再使用同一个 BinaryOperator 进行合并
📦 模拟并行归约的过程(简化版)
下面我们手动模拟一下并行归约的处理方式:
java
int reduce(List<Integer> ints, BinaryOperator<Integer> operator) {
int result = ints.get(0);
for (int i = 1; i < ints.size(); i++) {
result = operator.apply(result, ints.get(i));
}
return result;
}
现在使用它来模拟将列表拆分并并行处理:
java
List<Integer> ints = List.of(3, 6, 2, 1);
BinaryOperator<Integer> sum = (a, b) -> a + b;
int result1 = reduce(ints.subList(0, 2), sum); // 3 + 6 = 9
int result2 = reduce(ints.subList(2, 4), sum); // 2 + 1 = 3
int finalResult = sum.apply(result1, result2); // 9 + 3 = 12
System.out.println("sum = " + finalResult);
🟢 输出:
java
sum = 12
🔍 归约顺序的不同拆法(Associativity)
不管你怎么划分数据,以下不同的组合方式都应得到同样的结果:
3 + (6 + 2 + 1)(3 + 6) + (2 + 1)(3 + 6 + 2) + 1
💡 这种特性叫做:结合性(Associativity)。
🎓 什么是结合性(Associativity)?
一个二元操作符 op 被称为结合的 ,如果对于任意 a, b, c 都满足:
java
op(a, op(b, c)) == op(op(a, b), c)
✅ 结合的操作符:
- 加法:
(a + b) + c == a + (b + c) - 乘法:
(a * b) * c == a * (b * c) - 最大值:
max(max(a, b), c) == max(a, max(b, c))
❌ 非结合的操作符(举例):
java
BinaryOperator<String> nonAssociative = (a, b) -> a + "-" + b;
("a" + "-" + "b") + "-" + "c"≠"a" + "-" + ("b" + "-" + "c")- 输出为
"a-b-c"vs"a-b-c"是一样的,但你再嵌套其他逻辑就容易出错了
⚠️ 为什么结合性重要?
如果你的 BinaryOperator 不具有结合性:
- 并行处理可能产生不一致结果
- 不会报错 ,但结果可能每次都不同
- 更糟的是,有时结果看起来"对",但其实潜藏隐患
🧪 举例:
java
BinaryOperator<Double> subtract = (a, b) -> a - b;
List<Double> nums = List.of(100.0, 50.0, 25.0);
double res1 = subtract.apply(subtract.apply(100.0, 50.0), 25.0); // (100 - 50) - 25 = 25
double res2 = subtract.apply(100.0, subtract.apply(50.0, 25.0)); // 100 - (50 - 25) = 75
📛 显然这两个结果不同,说明减法不是结合的操作。
✅ 如何确保 BinaryOperator 是安全的?
- 选择已知的结合操作:加法、乘法、最大/最小值
- 避免带状态、不可预测的函数
- 测试不同组合是否得出相同结果
- 在业务允许的范围内,写清楚非结合操作的限制,尽量避免并行使用
📌 小结表格
| 特性 | 是否适用于 reduce() 并行处理? |
|---|---|
a + b |
✅ 是 |
Math.max(a, b) |
✅ 是 |
a - b |
❌ 否 |
a / b |
❌ 否 |
a + "-" + b |
⚠️ 小心,结果敏感于顺序 |
| 状态变更的操作(如打印) | ❌ 严禁! |
💬 总结
- 并行流本质上是拆分 + 局部归约 + 合并归约
- 使用
reduce()时一定要保证 结合性 - 否则可能会导致难以复现的 bug 或性能陷阱