并行流的行为与流不同

时间:2018-08-07 10:14:21

标签: java parallel-processing java-stream

我很难理解为什么并行流和流对于完全相同的语句给出不同的结果。

    List<String> list = Arrays.asList("1", "2", "3");
    String resultParallel = list.parallelStream().collect(StringBuilder::new,
            (response, element) -> response.append(" ").append(element),
            (response1, response2) -> response1.append(",").append(response2.toString()))
            .toString();
    System.out.println("ResultParallel: " + resultParallel);

    String result = list.stream().collect(StringBuilder::new,
            (response, element) -> response.append(" ").append(element),
            (response1, response2) -> response1.append(",").append(response2.toString()))
            .toString();

    System.out.println("Result: " + result);

ResultParallel:1、2、3

结果:1​​ 2 3

有人可以解释为什么会这样,以及我如何获得非并行版本以提供与并行版本相同的结果吗?

3 个答案:

答案 0 :(得分:12)

Java 8 Stream.collect方法具有以下签名:

<R> R collect(Supplier<R> supplier,
              BiConsumer<R, ? super T> accumulator,
              BiConsumer<R, R> combiner);

BiConsumer<R, R> combiner仅在并行流中被调用(以便将部分结果合并到单个容器中),因此第一个代码段的输出为:

ResultParallel: 1, 2, 3

sequential版本中,combiner不会被调用(请参阅此answer),因此以下语句将被忽略:

(response1, response2) -> response1.append(",").append(response2.toString())

结果不同:

1 2 3

如何解决?检查@Eugene的answer或此question and answers

答案 1 :(得分:8)

要了解原因出了错,请考虑javadoc中的问题。

  

accumulator-一种关联,无干扰,无状态的函数,必须将元素折叠到结果容器中。

     

combiner-一种关联,无干扰的无状态函数,它接受两个部分结果容器并将其合并,必须与累加器功能兼容。组合器功能必须将元素从第二个结果容器折叠到第一个结果容器中。

这是什么意思,元素是通过“累加”还是“组合”或两者的某种组合来收集都没有关系。但是在您的代码中,累加器和组合器使用不同分隔符进行连接。从Javadoc的要求来看,它们不是“兼容的”。

根据使用的是顺序流还是并行流,导致结果不一致。

  • 在并行情况下,流分为子流 1 ,由不同的线程处理。这导致每个子流的单独收集。然后将这些集合合并。

  • 在顺序情况下,不拆分流。取而代之的是,将流简单地累积到单个集合中,而无需进行合并。


观察:

  • 通常,对于执行简单转换的这种大小的流,parallelStream()可能会使速度变慢。

  • 在这种特定情况下,parallelStream()版本的瓶颈将成为合并步骤。这是一个串行步骤,它执行与整个串行管道相同数量的复制。因此,事实上,并行化肯定会使事情变慢。

  • 实际上,lambda行为不正确。它们在开头添加了一个额外的空间,如果使用了combiner,则将它们增加了一倍。一个更正确的版本是:

    String result = list.stream().collect(
        StringBuilder::new,
        (b, e) -> b.append(b.isEmpty() ? "" : " ").append(e),
        (l, r) -> l.append(l.isEmpty() ? "" : " ").append(r)).toString();
    
  • Joiner类是连接流的一种更简单,更有效的方法。 (来源:@Eugene)


1-在这种情况下,每个子流只有一个元素。对于更长的列表,通常将获得与工作线程一样多的子流,并且子流将包含多个元素。

答案 2 :(得分:7)

请注意,即使您将,中的空格替换为combiner,您的结果仍然会有所不同(略微更改了代码以使其更具可读性):

String resultParallel = list.parallelStream().collect(
            StringBuilder::new,
            (builder, elem) -> builder.append(" ").append(elem),
            (left, right) -> left.append(" ").append(right)).toString();

    String result = list.stream().collect(
            StringBuilder::new,
            (builder, elem) -> builder.append(" ").append(elem),
            (left, right) -> left.append(" ").append(right)).toString();


  System.out.println("ResultParallel: ->" + resultParallel + "<-"); // -> 1  2  3  4<-
  System.out.println("Result: ->" + result + "<-"); // -> 1 2 3 4<-

注意您的空间太多了。

java-doc具有提示:

  

组合器...必须与累加器功能兼容

如果您想加入,有一些更简单的选项,例如:

String.join(",", yourList)
yourList.stream().collect(Collectors.joining(","))