并行流与串行流

17

在Java 8中,一个并行流是否可能给出与串行流不同的结果? 根据我的信息,一个并行流和串行流是一样的,只不过被分成了多个子流。这是关于速度的问题。对所有元素的操作都完成后,在最后合并子流的结果。最终,我认为并行流和串行流的操作结果应该是相同的。所以我的问题是,这段代码是否可能给出不同的结果?如果有可能,为什么会发生?

int[] i = {1, 2, 5, 10, 9, 7, 25, 24, 26, 34, 21, 23, 23, 25, 27, 852, 654, 25, 58};
Double serial = Arrays.stream(i).filter(si -> {
    return si > 5;
}).mapToDouble(Double::new).map(NewClass::add).reduce(Math::atan2).getAsDouble();

Double parallel = Arrays.stream(i).filter(si -> {
    return si > 5;
}).parallel().mapToDouble(Double::new).map(NewClass::add).reduce(Math::atan2).getAsDouble();

System.out.println("serial: " + serial);
System.out.println("parallel: " + parallel);

public static double add(double i) {
    return i + 0.005;
}

结果如下:

serial: 3.6971567726175894E-23

parallel: 0.779264049587662

6
使用 atan2 进行缩减完全没有意义。例如,它不是可结合的。 - Paul Boddington
1
FYI: si -> { return si > 5; } 应该改为 si -> si > 5,并且您需要在 parallel() 之后进行过滤。 - Andreas
2
不,问题在于reduce需要一个可结合的函数。 - Andreas
3
@Andreas,你可以在流的起始和终止操作之间任意添加.parallel()方法,结果是相同的。 - Tagir Valeev
2
调用.mapToDouble(Double::new)将每个int扩展为double,将它们装箱成Double对象,然后再将它们拆箱为double值。如果要将int转换为double,则.mapToDouble(i->i)会更直接,跳过对象创建。但是更简单的方法是.asDoubleStream()... 如果您真的需要装箱的值,请使用Double::valueOf而不是Double::new - Holger
显示剩余3条评论
3个回答

13

reduce()的Java文档说:

使用一个可交换的累加函数,对该流的元素执行规约操作[...]累加器函数必须是一个可交换的函数。

单词"associative"链接到这个java文档:

如果满足以下内容,则运算符或函数op是可交换的:

 (a op b) op c == a op (b op c)

如果我们将其扩展到四个术语,则可以看出这对并行评估的重要性:

 a op b op c op d == (a op b) op (c op d)

因此,我们可以并行地评估(a op b)和(c op d),然后在结果上调用op。

可交换操作的示例包括数字加法、最小值、最大值和字符串连接。

正如@PaulBoddington在评论中提到的,atan2不是可交换的,因此不能用作约简操作。


无关的内容

您的流序列有点错乱。您应该在并行操作之后过滤,lambda可以缩短,并且不应该装箱双倍:

double parallel = Arrays.stream(i)
                        .parallel()           // <-- before filter
                        .filter(si -> si > 5) // <-- shorter
                        .asDoubleStream()     // <-- not boxing
                        .reduce(Math::atan2)
                        .getAsDouble();

7
不相关的部分完全不相关。.parallel() 可以放置在流水线的任何位置,其结果将保持不变。 - Tagir Valeev
@TagirValeev - 如果您提前并行处理,它不会更快运行吗? - ArtOfWarfare
@ArtOfWarfare,不,它不会。 - Tagir Valeev

4
当您使用并行流的reduce方法时,操作不会按特定顺序执行。
因此,如果您希望并行流产生可预测的结果,则reduce操作必须在任何顺序下都具有相同的答案。
例如,使用加法进行缩减是有意义的,因为加法是关联的。无论您做哪个操作,答案都是6
(1 + 2) + 3
1 + (2 + 3)

atan2不是结合的。

Math.atan2(Math.atan2(1, 2), 3) == 0.15333604941031637

Math.atan2(1, Math.atan2(2, 3)) == 1.0392451500584097

3
你的reduce方法会因元素顺序不同而产生不同的结果。
如果你使用并行流,则原始顺序不能保证。
如果你使用不同的归约方法(例如(x,y)->x+y),它就可以正常工作。

1
顺序始终相同。结果组合的方式不同。无序操作是可交换的。无关组合的操作是可结合的。在这里,顺序被保留,因此可交换性是不必要的。 - Tagir Valeev
@TagirValeev,这仅适用于有序流。 - the8472
1
@the8472,无序流肯定是无序的。 - Tagir Valeev
是的,但在没有排序串和并行串的情况下,它们可能会表现出不同的行为,这可能与 OP 的特定示例无关,但它确实适用于一般问题。 - the8472

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接