前提:我已经阅读了这个问题和其他问题,但我需要一些澄清。
我知道Stream.forEach方法在处理并行流时会产生差异(不仅仅是这个),这就解释了为什么会这样
1 2 3 4
| //1
Stream. of("one", "two", "three", "four", "five", "six")
. parallel()
. forEachOrdered(item -> System. out. print(item )); |
版画
1
| one two three four five six |
但是当涉及到中间操作时,当并行化流时,不再保证顺序。 所以这段代码
1 2 3 4 5
| //2
Stream. of("one", "two", "three", "four", "five", "six")
. parallel()
. peek(item -> System. out. print(item ))
. forEachOrdered(item -> System. out. print("")); |
会印出类似的东西
1
| four six five one three two |
说forEachOrdered方法只影响其自身执行中元素的顺序是否正确? 直觉上,我认为//1的例子完全相同
1 2 3 4 5 6
| //3
Stream. of("one", "two", "three", "four", "five", "six")
. parallel()
. peek(item -> System. out. print("")) //or any other intermediate operation
. sequential()
. forEach(item -> System. out. print(item )); |
我的直觉是错的吗? 我错过了整个机制的一些内容吗?
-
不,你不能将一个op转为并行,而另一个转为顺序。 传递给forEachOrdered的操作仍然可能由不同的线程调用,但它们将适当地同步。
-
@Holger,我不明白......
-
在一个点上同步的多个线程仍然与单线程执行不同。
-
@Holger我想我理解你的意思。 但是对于我的具体情况,我只关注元素顺序而不考虑线程重叠问题,//1 //3在概念上是等价的吗?
你是对的,因为forEachOrdered的行动所做的保证只适用于那个动作而没有其他任何内容。但是假设这与.sequential().forEach(…)相同是错误的。
sequential会将整个流管道转换为顺序模式,因此,传递给forEach的操作将由同一个线程执行,但也会执行前面的peek操作。对于大多数中间操作,parallel或sequential的确切位置是无关紧要的,并指定两者都没有意义,因为只有最后一个是相关的。
此外,即使在当前实现中没有任何后果,使用forEach时仍然无法保证订购。这在"Stream.forEach是否遵循顺序流的遭遇顺序?"中讨论。
Stream.forEachOrdered的文档说明:
This operation processes the elements one at a time, in encounter order if one exists. Performing the action for one element happens-before performing the action for subsequent elements, but for any given element, the action may be performed in whatever thread the library chooses.
因此,动作可能会被不同的线程调用,如Thread.currentThread()可感知但不会同时运行。
此外,如果流具有遭遇顺序,则它将在此处重构。这个答案揭示了遭遇顺序和处理顺序的区别。
-
"顺序会将整个流管道变为顺序模式",这是我完全遗漏的东西!
#3代码在概念上不等同于#1,因为parallel()或sequential()调用影响整个流,而不仅仅影响后续操作。因此在#3情况下,整个过程将按顺序执行。实际上#3案例类似于实际可以更改并行/顺序模式时Stream API的早期设计。这被认为是不必要的复杂化(并且实际上添加了问题,例如,参见此讨论),因为通常您只需要更改模式以使终端操作有序(但不一定是顺序的)。因此添加了forEachOrdered()并更改了parallel()/sequential()语义以影响整个流(请参阅此变更集)。
基本上你是对的:在并行流中,没有中间操作的订单保证。如果需要按特定顺序执行它们,则必须使用顺序流。