[英]Doesn't Stream.parallel() update the characteristics of spliterator?
這個問題是基於這個問題的答案Stream.of 和 IntStream.range 有什么區別?
由於IntStream.range
生成已排序的 stream,因此 output 到以下代碼只會生成 output 為0
:
IntStream.range(0, 4)
.peek(e -> System.out.println(e))
.sorted()
.findFirst();
拆分器也將具有SORTED
特征。 下面的代碼返回true
:
System.out.println(
IntStream.range(0, 4)
.spliterator()
.hasCharacteristics(Spliterator.SORTED)
);
現在,如果我在第一個代碼中引入一個parallel()
,那么正如預期的那樣,output 將包含從0
到3
的所有 4 個數字,但順序是隨機的,因為 stream 由於parallel()
而不再排序。
IntStream.range(0, 4)
.parallel()
.peek(e -> System.out.println(e))
.sorted()
.findFirst();
這將以任何順序產生如下所示的內容:
2
0
1
3
所以,我希望SORTED
屬性由於parallel()
而被刪除。 但是,下面的代碼也返回true
。
System.out.println(
IntStream.range(0, 4)
.parallel()
.spliterator()
.hasCharacteristics(Spliterator.SORTED)
);
為什么parallel()
不改變SORTED
屬性? 並且由於打印了所有四個數字, Java 如何意識到SORTED
未排序,即使 SORTED 屬性仍然存在?
究竟如何做到這一點在很大程度上是一個實現細節。 您必須深入挖掘源代碼才能真正了解原因。 基本上,並行和順序流水線的處理方式不同。 查看AbstractPipeline.evaluate
,它檢查isParallel()
,然后根據管道是否並行執行不同的操作。
return isParallel()
? terminalOp.evaluateParallel(this, sourceSpliterator(terminalOp.getOpFlags()))
: terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags()));
如果您再查看SortedOps.OfInt
,您會發現它覆蓋了兩個方法:
@Override
public Sink<Integer> opWrapSink(int flags, Sink sink) {
Objects.requireNonNull(sink);
if (StreamOpFlag.SORTED.isKnown(flags))
return sink;
else if (StreamOpFlag.SIZED.isKnown(flags))
return new SizedIntSortingSink(sink);
else
return new IntSortingSink(sink);
}
@Override
public <P_IN> Node<Integer> opEvaluateParallel(PipelineHelper<Integer> helper,
Spliterator<P_IN> spliterator,
IntFunction<Integer[]> generator) {
if (StreamOpFlag.SORTED.isKnown(helper.getStreamAndOpFlags())) {
return helper.evaluate(spliterator, false, generator);
}
else {
Node.OfInt n = (Node.OfInt) helper.evaluate(spliterator, true, generator);
int[] content = n.asPrimitiveArray();
Arrays.parallelSort(content);
return Nodes.node(content);
}
}
如果它是順序管道,最終將調用opWrapSink
,而當它是並行 stream 時,將調用opEvaluateParallel
(顧名思義)。 請注意,如果管道已經排序(只是將其原封不動地返回), opWrapSink
不會對給定的接收器執行任何操作,但opEvaluateParallel
始終評估拆分器。
另請注意,並行性和排序性並不相互排斥。 您可以擁有具有這些特性的任意組合的 stream。
“排序”是Spliterator
的一個特征。 從技術上講,這不是Stream
的特征(就像“並行”一樣)。 當然, parallel
可以創建一個 stream 和一個全新的分離器(從原始分離器中獲取元素)和全新的特性,但是當你可以重復使用相同的分離器時,為什么要這樣做呢? 我想你在任何情況下都必須以不同的方式處理並行和順序流。
考慮到ForkJoinPool
用於並行流並且它的工作原理基於工作竊取,您需要退后一步,想想一般如何解決這樣的問題。 如果您也知道Spliterator
的工作原理,那將非常有幫助。 這里有一些細節。
你有一個 Stream,你將它“拆分”(非常簡化)成更小的部分,並將所有這些部分交給ForkJoinPool
執行。 所有這些部分都是由單獨的線程獨立處理的。 由於我們在這里討論線程,顯然沒有事件順序,事情是隨機發生的(這就是你看到隨機順序輸出的原因)。
如果您的 stream 保留訂單,終端操作也應該保留它。 因此,雖然中間操作以任何順序執行,但您的終端操作(如果 stream 到該點是有序的)將以有序的方式處理元素。 稍微簡化一下:
System.out.println(
IntStream.of(1,2,3)
.parallel()
.map(x -> {System.out.println(x * 2); return x * 2;})
.boxed()
.collect(Collectors.toList()));
map
將以未知的順序處理元素( ForkJoinPool
和線程,記住這一點),但collect
將按“從左到右”的順序接收元素。
現在,如果我們將其推斷到您的示例:當您調用parallel
時,stream 被分成小塊並進行處理。 例如,看看這是如何拆分的(一次)。
Spliterator<Integer> spliterator =
IntStream.of(5, 4, 3, 2, 1, 5, 6, 7, 8)
.parallel()
.boxed()
.sorted()
.spliterator()
.trySplit(); // trySplit is invoked internally on parallel
spliterator.forEachRemaining(System.out::println);
在我的機器上打印1,2,3,4
。 這意味着內部實現將 stream 拆分為兩個Spliterator
: left
和right
。 left
有[1, 2, 3, 4]
,右邊有[5, 6, 7, 8]
。 但事實並非如此,這些Spliterator
還可以進一步拆分。 例如:
Spliterator<Integer> spliterator =
IntStream.of(5, 4, 3, 2, 1, 5, 6, 7, 8)
.parallel()
.boxed()
.sorted()
.spliterator()
.trySplit()
.trySplit()
.trySplit();
spliterator.forEachRemaining(System.out::println);
如果您嘗試再次調用trySplit
,您將得到null
- 意思就是,就是這樣,我不能再拆分了。
因此,您的 Stream: IntStream.range(0, 4)
將被拆分為 4 個拆分器。 所有的工作都是由一個線程單獨完成的。 如果你的第一個線程知道它當前工作的這個Spliterator
是“最左邊的”,就是這樣。 線程的 rest 甚至不需要啟動它們的工作 - 結果是已知的。
另一方面,這個具有“最左邊”元素的Spliterator
可能只在最后啟動。 因此,前三個可能已經完成了他們的工作(因此在您的示例中調用了peek
),但它們不會“產生”所需的結果。
事實上,這是在內部完成的。 您不需要了解代碼 - 但流程和方法名稱應該是顯而易見的。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.