[英]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.