【问题标题】:Encounter order friendly/unfriendly terminal operations vs parallel/sequential vs ordered/unordered streams遇到订单友好/不友好的终端操作 vs 并行/顺序 vs 有序/无序流
【发布时间】:2017-12-10 18:53:36
【问题描述】:

this question 的启发,我开始研究有序流与无序流、并行流与顺序流以及尊重顺序的终端操作与不尊重顺序的终端操作。

在链接问题的一个答案中,显示了与此类似的代码:

List<Integer> ordered = Arrays.asList(
    1, 2, 3, 4, 4, 3, 2, 1, 1, 2, 3, 4, 4, 3, 2, 1, 1, 2, 3, 4);
List<Integer> result = new CopyOnWriteArrayList<>();

ordered.parallelStream().forEach(result::add);

System.out.println(ordered);
System.out.println(result);

而且列表确实不同。 unordered 列表甚至会从一个运行变为另一个,表明结果实际上是不确定的。

所以我创建了另一个示例:

CopyOnWriteArrayList<Integer> result2 = ordered.parallelStream()
        .unordered()
        .collect(Collectors.toCollection(CopyOnWriteArrayList::new));

System.out.println(ordered);
System.out.println(result2);

我希望看到类似的结果,因为流既是并行的又是无序的(也许unordered() 是多余的,因为它已经是并行的了)。但是,结果列表是有序的,即它等于源列表。

所以我的问题是为什么收集的列表是有序的? collect 是否总是尊重遭遇顺序,即使是并行的、无序的流?是特定的Collectors.toCollection(...) 收集器强制遇到顺序吗?

【问题讨论】:

  • 非常有趣的问题,但最后一个是题外话。你最好把它从你的帖子中删除。
  • 是的,这样会更好。
  • 顺便说一句,它正在慢慢成为流的 SO 标准;这是 Holger 的必读内容:stackoverflow.com/questions/41894173/…
  • unordered()not 冗余的,因为并行并不意味着无序。这已经解释了here。说无序终端操作“不尊重”遭遇顺序是错误的。它的无序特性意味着顺序对结果没有意义,例如在对整数求和或收集到HashSet 时。当您明确地说unordered() 时,您也是在说顺序无关紧要,即使您可以识别结果列表中的顺序。如果订单与您无关,那毫无疑问……

标签: java java-8 java-stream


【解决方案1】:

Collectors.toCollection 返回一个缺少Collector.Characteristics.UNORDERED 特征的Collector。另一个指定 Collector.Characteristics.UNORDERED 的收集器可能会有不同的行为。

也就是说:“无序”意味着不保证,而不是保证变化。如果图书馆发现将无序集合视为有序集合最容易,则允许这样做,并且允许该行为在星期二或满月时更改发布。

(另请注意,如果您要使用并行流,Collectors.toCollection 不需要您使用并发收集实现;toCollection(ArrayList::new) 可以正常工作。那是因为收集器没有 Collector.Characteristics.CONCURRENT 特性,因此它使用的收集策略适用于非并发收集,即使是并行流。)

如果您使用无序流但收集器不是UNORDERED,反之亦然,我怀疑您从框架中得到任何保证。如果有一张桌子,它会说“这里是 DRAGONS UNDEFINED BEHAVIOR”。我还期望这里不同类型的链式操作会有所不同,例如Eugene 提到 findFirst 在这里有所不同,尽管 findFirst 本质上是一个有序的操作——unordered().findFirst() 变得等同于 findAny()

对于Stream.collect,我相信当前的实现有三种策略可供选择:

  • 顺序:启动一个累加器,将元素累加到其中(按照遇到的顺序,因为你为什么要费心去打乱元素?只需按照你得到它们的顺序接受它们),调用完成器。
  • 并行执行、并发收集器和流或收集器是无序的:一个累加器,对输入进行分片,工作线程处理来自每个分片的元素,并在它们准备好时将元素添加到累加器,调用终结器。
  • 并行执行,其他:将输入分片到 N 个分片中,每个分片依次累加到自己的不同累加器中,累加器与组合器函数组合,调用终结器。

【讨论】:

  • 因此,在这种情况下,保留订单这一事实是一个实现细节,以后可能会改变,对吧? toCollection 承诺遵守顺序,但流是无序的,所以输出可能与源的顺序不同,对吧?
  • @JBNizet 我会说是的,这将符合规范。
  • @JBNizet 完全正确。在链接的答案中,这个确切的差异是使用 findFirst 提供的,实际上从 8 变为 9。
  • @JBNizet 附带说明一下,通常允许实现这样做。另一个例子是 AtomicInteger#weakCompareAndSetAtomicInteger#compareAndSet 在 java-9 之前做了相同的事情
  • @Eugene 我更多地指的是方法名称中“first”这个词的存在,因为它直观地是一个有序的操作。我已经说过unordered().findFirst() 等价于findAny(),一个无序操作。我们在同一个页面上,只是措辞不同。
【解决方案2】:

当前实现下,我检查了 java-8 和 java-9,对于非并发收集器的collect 阶段,无序标志被忽略(Collector.Characteristics.UNORDERED 未设置)。允许实现这样做,有点类似question

在您链接的同一个问题中,我确实提供了 findFirst 实际上如何从 jdk-8 更改为 jdk-9 的示例。

【讨论】:

    【解决方案3】:

    文档Stream#collect已经提到:

    当并行执行时,多个中间结果可能被实例化填充合并,以保持可变数据结构的隔离。因此,即使与非线程安全的数据结构(例如 ArrayList)并行执行,不需要额外的同步来进行并行缩减。

    这意味着Stream#collect 做了两个主要的事情:splitmerge

    但我在 jdk-8 中有一个特殊示例,您可以获取不同的结果,:)。当Stream#generate创建无序流时,你可以在Collectors#toList上获取不同的结果,例如:

    Set<Set<Integer>> result = IntStream.range(0, 10).mapToObj(__ -> {
            return unordered().parallel().collect(toSet());
    }).collect(toSet());
    
    assert result.each.size() == 100000; // ok
    //                   v--- surprised, it was pass 
    assert result.size() > 1; 
    

    Stream<Integer> unordered() {
        AtomicInteger counter = new AtomicInteger();
        return Stream.generate(counter::getAndIncrement).limit(10000);
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-01-12
      • 1970-01-01
      • 2015-06-02
      相关资源
      最近更新 更多