【问题标题】:Parallel stream vs serial stream并行流与串行流
【发布时间】:2015-09-26 17:28:58
【问题描述】:

在 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

【问题讨论】:

  • 减少使用atan2 完全没有意义。例如,它不是关联的。
  • 仅供参考:si -> { return si > 5; } 应该只是 si -> si > 5,并且您希望在 parallel() 之后 进行过滤。
  • 不,问题是reduce 需要一个关联函数
  • @Andreas,你可以把.parallel()放在流启动和终端操作之间的任意位置,结果是一样的。
  • 调用.mapToDouble(Double::new) 将每个int 扩大到double,将它们装箱为Double 对象,然后将它们拆箱为double 值。如果您想将int 转换为double.mapToDouble(i->i) 会更直接,跳过对象创建。但更简单的是.asDoubleStream()...而且,如果您真的需要装箱值,请使用Double::valueOf 而不是Double::new

标签: java lambda parallel-processing java-8 java-stream


【解决方案1】:

reduce() 的 javadoc 说:

使用关联累加函数对该流的元素执行缩减,[...]累加器函数必须是关联函数。

"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 装箱:

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

【讨论】:

  • 不相关的部分是完全不相关的。 .parallel() 可能位于管道的任何位置,结果将是相同的。
  • @TagirValeev - 如果你早点并行它会不会运行得更快?
  • @ArtOfWarfare,不,不会。
【解决方案2】:

当您将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),它就可以正常工作。

    【讨论】:

    • 顺序总是一样的。结果组合的方式不同。与顺序无关的操作是可交换的。与组合无关的操作是关联的。这里顺序保持不变,所以交换律是不必要的。
    • @TagirValeev,仅适用于有序流。
    • @the8472,无序流肯定是无序的。
    • 是的,但是在没有排序的情况下,串行和并行流的行为可能会有所不同,这可能与 OP 的特定示例无关,但它确实适用于一般问题。
    猜你喜欢
    • 1970-01-01
    • 2014-07-24
    • 1970-01-01
    • 1970-01-01
    • 2022-06-16
    • 1970-01-01
    • 2020-08-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多