并行流非并发无序收集器

时间:2016-11-30 08:34:29

标签: java-8 java-stream

假设我有这个自定义收藏家:

  public class CustomToListCollector<T> implements Collector<T, List<T>, List<T>> {

     @Override
     public Supplier<List<T>> supplier() {
         return ArrayList::new;
     }

     @Override
     public BiConsumer<List<T>, T> accumulator() {
         return List::add;
     }

     @Override
     public BinaryOperator<List<T>> combiner() {
         return (l1, l2) -> {
            l1.addAll(l2);
            return l1;
         };
     }

     @Override
     public Function<List<T>, List<T>> finisher() {
         return Function.identity();
     }

     @Override
     public Set<java.util.stream.Collector.Characteristics> characteristics() {
         return EnumSet.of(Characteristics.IDENTITY_FINISH, Characteristics.UNORDERED);
     }
}

这正是收藏家#toList 实施,只有一个细微差别:还添加了UNORDERED特征。

我会假设运行此代码:

    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);

    for (int i = 0; i < 100_000; i++) {
        List<Integer> result = list.parallelStream().collect(new CustomToListCollector<>());
        if (!result.equals(list)) {
            System.out.println(result);
            break;
        }
    }

实际上应该产生一些结果。但事实并非如此。

我已经看了一下罩子。 ReferencePipeline#collect 首先检查流是并行的,收集器是并发的还是收集器是无序的。缺少并发,因此它通过从此收集器创建TerminalOp来委托方法评估。引擎盖下的是一个ReducingSink,实际上关心收集器是否无序

         return new ReduceOp<T, I, ReducingSink>(StreamShape.REFERENCE) {
        @Override
        public ReducingSink makeSink() {
            return new ReducingSink();
        }

        @Override
        public int getOpFlags() {
            return collector.characteristics().contains(Collector.Characteristics.UNORDERED)
                   ? StreamOpFlag.NOT_ORDERED
                   : 0;
        }
    }; 

我没有进一步调试,因为它变得非常复杂。

因此,这里可能有一条捷径,有人可以解释我所缺少的东西。它是一个并行流,用于收集非并发无序收集器中的元素。难道不存在线程如何将结果组合在一起的顺序吗?如果不是,这里的订单是如何强加的(由谁)?

2 个答案:

答案 0 :(得分:6)

请注意,使用list .parallelStream() .unordered() .collect(Collectors.toList())时结果相同,在任何一种情况下,当前实现中都不使用无序属性。

但是让我们稍微改变一下设置:

List<Integer> list = Collections.nCopies(10, null).stream()
    .flatMap(ig -> IntStream.range(0, 100).boxed())
    .collect(Collectors.toList());
List<Integer> reference = new ArrayList<>(new LinkedHashSet<>(list));

for (int i = 0; i < 100_000; i++) {
    List<Integer> result = list.parallelStream()
      .distinct()
      .collect(characteristics(Collectors.toList(), Collector.Characteristics.UNORDERED));
    if (!result.equals(reference)) {
        System.out.println(result);
        break;
    }
}

使用this answer characteristics收集器工厂 有趣的结果是,在1.8.0_60之前的Java 8版本中,此具有不同的结果。如果我们使用具有不同身份的对象而不是规范Integer实例,我们可以检测到在这些早期版本中,不仅列表的顺序不同,而且结果列表中的对象不是第一个遇到的实例

因此,终端操作的无序特征传播到流,影响distinct()的行为,类似于skiplimit的行为,正如所讨论的那样herehere

如第二个链接线程中所讨论的,反向传播已被完全删除,这在第二次思考时是合理的。对于distinctskiplimit,源的顺序是相关的,并且忽略它只是因为在后续阶段中将忽略该顺序是不对的。因此,可以从反向传播中受益的唯一剩余的有状态中间操作将是sorted,这将在之后忽略该订单时变得过时。但是将sorted与无序接收器相结合更像是编程错误......

对于无国籍中间操作,订单无论如何都是无关紧要的。流处理的工作原理是将源拆分为块,在合并到结果容器之前,将所有无状态中间操作独立应用于其元素并收集到本地容器中。所以合并步骤是唯一的地方,尊重或忽略(块的)顺序会对结果产生影响,也许会对性能产生影响。

但影响不是很大。当您实施这样的操作时,例如,通过ForkJoinTask s,您只需将任务分成两部分,等待完成并合并它们。或者,任务可以将块拆分为子任务,就地处理其剩余块,等待子任务并合并。在任何一种情况下,由于启动任务具有对相邻任务的引用这一事实,因此按顺序合并结果是自然的。要改为与不同的块合并,首先必须以某种方式找到相关的子任务。

与其他任务合并的唯一好处是,如果任务需要不同的时间来完成,您可以与第一个已完成的任务合并。但是当等待Fork / Join框架中的子任务时,线程将不会处于空闲状态,框架将使用该线程处理其中的其他待处理任务。因此,只要主要任务被分成足够的子任务,就会有完全的CPU利用率。此外,分裂器试图分成均匀的块以减少计算时间之间的差异。很可能,替代无序合并实现的好处并不能证明代码重复的合理性,至少在当前实现方面是这样。

尽管如此,报告无序特征允许实现在有益的情况下使用它,并且实现可以改变。

答案 1 :(得分:2)

这不是一个真正的答案本身,但如果我添加更多代码和评论,我猜它会变得太多。

这是另一个有趣的事情,实际上它让我意识到我在评论中错了。

spliterator标志需要与所有终端操作标志和中间标志合并。

我们的分裂者旗帜(由StreamOpFlags报道):95;这可以从 AbstractSpliterator#sourceSpliterator(int terminalFlags)调试。

这就是下面这一行报告为真的原因:

 System.out.println(StreamOpFlag.ORDERED.isKnown(95)); // true

同时我们的终端收藏家的特征是32:

System.out.println(StreamOpFlag.ORDERED.isKnown(32)); // false

结果:

int result = StreamOpFlag.combineOpFlags(32, 95); // 111
System.out.println(StreamOpFlag.ORDERED.isKnown(result)); // false

如果你想到这一点,那就完全有道理了。列表有订单,我的自定义收集器没有=&gt;订单不会保留。

底线:UNORDERED标志保留在生成的Stream中,但内部没有任何操作。他们可能,但他们选择不这样做。