了解并行流和收集器顺序

问题描述 投票:0回答:2

我试图理解为什么当我在下面的代码片段中从

y.addAll(x)
更改为
x.addAll(y)
时会有不同:

List<Integer> result = List.of(1, 2)
    .parallelStream() 
    .collect(
        ArrayList::new,
        (x, y) -> x.add(y),
        (x, y) -> y.addAll(x)
    );
System.out.println(result);

我知道,当我使用

parallelStream
时,一次会运行多个线程。

collect
有三个参数;前两个参数我理解。使用第三个参数,我知道x,y是子流,它们是
ArrayList
,但我不明白为什么每种情况下结果都不同。我希望他们是一样的。

  • (x, y) -> y.addAll(x) // output: [1]

  • (x, y) -> x.addAll(y) // output: [1, 2]

java java-stream java-11
2个回答
16
投票

为什么一个是正确的,另一个则不正确

来自Stream#collect

的Javadocs(特别是最后一个参数,强调我的):

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

类似地,

a.addAll(b)

会将所有从

b
a
 的元素相加,但反之则不然。它从参数中获取信息并修改接收器。
因此,该方法的约定指定您必须将 lambda 的第二个参数合并到第一个参数中。

如果您执行

(x, y) -> x.addAll(y)

,它将按照合同将

y

的所有元素添加到
x
中。但是,对于 
(x, y) -> y.addAll(x)
,您将其添加到第二个元素,导致 
y
 的元素未添加到 
x
,然后这些元素在结果中丢失。
发生了什么

这样做是因为并行流将处理分成多个块,其中不同的线程处理不同的块。处理之后,它需要将元素合并在一起,这是使用组合器完成的(最后一个 lambda 表达式,就是您谈到的那个)。该组合器需要能够将两个元素“组合”在一起,然后将第一个参数用于进一步处理,而第二个参数则被丢弃。

假设我们有数字 1 和 2,如您的示例中所示,并假设一个线程处理包含 1 的块,另一个线程处理包含 2 的块。收集时,每个线程首先在

ArrayList 之后创建一个新的 ArrayList::new

在你的代码中。然后,线程将其相应块的元素添加到列表中,从而生成两个列表,每个列表各有一个元素(第一个线程 1,另一个线程 2)。当两个线程都完成时,调用组合器来合并/组合结果。使用

x.addAll(y)

,它将第二个列表添加到第一个列表,然后返回并产生正确的结果。但是,使用 
y.addAll(x)
 时,它将第一个列表的元素添加到第二个列表中,但 Java 假设您想要第一个列表(因为这就是您应该修改的内容),因此收集返回不包含元素的第一个列表由第二个线程处理。

我已经重命名了一些变量,以更好地说明正在发生的事情。

List<Integer> result = List.of( 1, 2 ) .parallelStream() .collect( ArrayList::new, (thisList,value) -> thisList.add( value ), (thisList,theOtherList) -> thisList.addAll( theOtherList ) ); System.out.println( result );

7
投票
方法

Stream::collect

返回
thisList

当您将调用的第三个参数更改为 
collect()
 时,如下所示:

… (thisList,theOtherList) -> theOtherList.addAll( thisList ) …

您将 
thisList

的内容添加到
theOtherList
中,

thisList

 保持不变,这意味着 
theOtherList
 的内容未添加到其中。
结果只是
1
纯属巧合。

© www.soinside.com 2019 - 2024. All rights reserved.