【问题标题】:Stream.reduce always preserving order on parallel, unordered streamStream.reduce 始终保持并行无序流的顺序
【发布时间】:2018-01-12 21:57:44
【问题描述】:

我已经解决了之前的几个问题,例如 Encounter order preservation in java streamthis Brian Goetz 的回答,以及 Stream.reduce() 的 javadoc 和 java.util.stream 包 javadoc,但我仍然无法掌握以下内容:

获取这段代码:

  public static void main(String... args) {
    final String[] alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
    System.out.println("Alphabet: ".concat(Arrays.toString(alphabet)));
    System.out.println(new HashSet<>(Arrays.asList(alphabet))
          .parallelStream()
          .unordered()
          .peek(System.out::println)
          .reduce("", (a,b) -> a + b, (a,b) -> a + b));
  }

为什么减少总是*保留相遇顺序?

  • 到目前为止,经过数十次运行,输出是相同的

【问题讨论】:

  • 要添加到 Joe C 的答案中,并行流期望减少(或任何折叠)关联性 (a+b)+c=a+(b+c) 将起作用,但不需要可交换性。尽管数字上的 + 是可交换的,但就字符串连接而言 + 不是:“a”+“b”不是“b”+“a”。所有并行流算法都假设折叠(归约)不是可交换的,并保证在合并时保留左右边。最好写.reduce("", (acc,x) -&gt; acc + x, (a,b) -&gt; a + b) 表明您将 x 添加到累加器,并且两个 lambda 不一定相同。
  • “所有并行流算法都假设折叠(归约)不是可交换的,并保证在合并时保留左右边。” - 这是在哪里记录的?
  • unordered() 表示订单未定义,并不是说它保证与原始订单不同或保证会改变。它可以是实现选择的任何内容。
  • 路易斯,这是真的。该文档包括大量使用“可能”的工作。这是否意味着我正在使用的实现(openjdk 版本“1.8.0_131”)恰好总是保留遇到顺序?
  • @GeorgeAristy 看到我的回答......但基本上 - 是的

标签: java sorting java-8 java-stream reduce


【解决方案1】:

首先unordered 并不暗示实际的洗牌;它所做的一切都是为 Stream 管道设置一个标志 - 以后可以利用它。

源元素的洗牌可能比流管道本身上的操作更昂贵,因此实现可能选择不这样做(就像在这种情况下一样)。

目前(测试并查看了源代码)jdk-8jdk-9 - reduce 没有考虑到这一点。请注意,这很可能在未来的构建或发布中发生变化。

此外,当您说unordered 时,您实际上的意思是您不关心该顺序,并且返回相同结果的流并不违反该规则。

例如,注意 this 问题/答案说明 findFirst 例如(只是另一个终端操作)已更改为在 java-9 中而不是 java-8 中考虑 unordered

【讨论】:

  • 谢谢。我认为我的困惑源于对洗牌得到保证的期望。
  • 问题是关于并行流的。而且我还希望reduce会特别是如果流被标记为无序时会同时获取任何元素,多个元素,然后开始以随机先到先得的方式合并它。我们在这里并不是指有意改组元素。
  • @RenatoIvancic 或unordered 什么都不做,只是做同样的事情——因为我们不关心订单。您需要在这里提出什么实际问题/澄清?
  • 我不明白你对洗牌的定义。我希望使用并行流的 .reduce() 方法会产生多个线程并开始将字符串与累加器和组合器连接起来。由于无法在正确的并行执行中保证顺序,我仍然想知道如何对所有内容进行排序。或者它在 .reduce() 本身的末尾有一些步骤,它在并行执行后对元素进行排序。我必须做一些测试并检查源代码。
  • @RenatoIvancic 我明白了,让我试着解释一下。 除非另有说明,否则每个终端操作(包括reduce)都将保留初始来源的相遇顺序(认为List 不是 Set),除非您打破它(list.stream().unordered())。目前,没有以任何形式实现为reduce 调用unordered。为什么?由于实现是在内部进行的,实现这一目标的唯一方法是故意改组初始源。不走这样的路线。
【解决方案2】:

为了帮助解释这一点,我将把这个字符串的范围缩小到ABCD

并行流会将字符串分成两部分:ABCD。当我们稍后将它们组合起来时,AB 端的结果将是传递给函数的第一个参数,而CD 端的结果将是传递给函数的第二个参数。这与两者中的哪一个实际上先完成无关。

unordered 操作符会影响流上的一些操作,例如limit 操作,它不会影响简单的reduce

【讨论】:

  • 顺便提一下,HashMap 返回的流已经是无序的。
  • joe C,分流并行处理后,顺序如何保留?
  • @JoeC 好吧.. 这有点不对劲,目前 unordered 没有考虑到reduce - 它很容易改变......
  • @GeorgeAristy 这是一个深入的实现细节 - 但 大量 终端操作是精心制作的以保持顺序 - 即 源遇到顺序跨度>
【解决方案3】:

TLDR:.reduce() 并不总是保持顺序,其结果基于流拆分器的特性。

分离器

流的遭遇顺序取决于流spliterator(之前没有提到的答案)。

根据源流有不同的拆分器。您可以从这些集合的源代码中获取拆分器的类型。

HashSet -> HashMap#KeySpliterator = 未排序

ArrayDeque = 有序

ArrayList = 有序

TreeSet -> TreeMap#Spliterator = 有序和排序

logicbig.com - Ordering logicbig.com - Stateful vs Stateless

此外,您可以应用.unordered() 中间流操作,指定流中的以下操作不应依赖于排序。

受拆分器和.unordered() 方法使用影响的流操作(主要是有状态的)是:

  • .findFirst()
  • .limit()
  • .skip()
  • .distinct()

这些操作将根据流的 order 属性及其拆分器为我们提供不同的结果。

.peek() 方法不考虑排序,如果流并行执行,它将始终以无序方式打印/接收元素。

.reduce()

现在是终端.reduce() 方法。中间操作.unordered() 对拆分器的类型没有任何影响(如@Eugene 所述)。但重要的是,它仍然与源拆分器中的一样。如果 source spliterator 是有序的,则 .reduce() 的结果将是有序的,如果 source 是无序的,则 .reduce() 的结果将是无序的。

您正在使用新的HashSet&lt;&gt;(Arrays.asList(alphabet)) 来获取流的实例。它的拆分器是无序的。您得到有序的结果只是一个巧合,因为您使用单个字母字符串作为流的元素,而无序的结果实际上是相同的。现在,如果您将其与数字混合或将其与小写和大写混合,那么这不再适用。例如采用以下输入,第一个是您发布的示例的子集:

HashSet .reduce() - 无序

"A","B","C","D","E","F" -> "ABCDEF"
"a","b","c","1","2","3","A","B","C" -> "a1Ab2Bc3C"
"Apple","Orange","Banana","Mango" -> "AppleMangoOrangeBanana"

TreeSet .reduce() - 有序、有序

"A","B","C","D","E","F" -> "ABCDEF"
"a","b","c","1","2","3","A","B","C" -> "123ABCabc"
"Apple","Orange","Banana","Mango" -> "AppleBananaMangoOrange"

ArrayList .reduce() - 有序

"A","B","C","D","E","F" -> "ABCDEF"
"a","b","c","1","2","3","A","B","C" -> "abc123ABC"
"Apple","Orange","Banana","Mango" -> "AppleOrangeBananaMango"

您会发现,仅使用字母源流测试 .reduce() 操作可能会导致错误的结论。

答案是.reduce() 并不总是保持顺序,它的结果是基于流拆分器的特性。

【讨论】:

  • 这应该是被接受的答案。谢谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-11-19
  • 1970-01-01
  • 1970-01-01
  • 2020-06-27
  • 2020-05-25
  • 2021-04-15
相关资源
最近更新 更多