【问题标题】:Why AtomicInteger based Stream solutions are not recommended?为什么不推荐基于 AtomicInteger 的 Stream 解决方案?
【发布时间】:2018-11-16 00:37:33
【问题描述】:

假设我有这份水果清单:-

List<String> f = Arrays.asList("Banana", "Apple", "Grape", "Orange", "Kiwi");

我需要在每个水果前面加上一个序列号并打印出来。水果的顺序或序列号无关紧要。所以这是一个有效的输出:-

4. Kiwi
3. Orange
1. Grape
2. Apple
5. Banana

解决方案 #1

AtomicInteger number = new AtomicInteger(0);

String result = f.parallelStream()
        .map(i -> String.format("%d. %s", number.incrementAndGet(), i))
        .collect(Collectors.joining("\n"));

解决方案 #2

String result = IntStream.rangeClosed(1, f.size())
        .parallel()
        .mapToObj(i -> String.format("%d. %s", i, f.get(i - 1)))
        .collect(Collectors.joining("\n"));

问题

为什么解决方案 #1 是不好的做法?我在很多地方都看到基于AtomicInteger 的解决方案不好(比如this answer),特别是在并行流处理中(这就是我在上面使用并行流来尝试遇到问题的原因)。

我查看了这些问题/答案:-
In which cases Stream operations should be stateful?
Is use of AtomicInteger for indexing in Stream a legit way?
Java 8: Preferred way to count iterations of a lambda?

他们只是提到(除非我错过了什么)“可能会出现意想不到的结果”。像什么?在这个例子中会发生吗?如果没有,你能举个例子吗?

至于“不保证映射器函数的应用顺序”,嗯,这就是并行处理的本质,所以我接受它,而且顺序不在这个特定的例子中无关紧要。

AtomicInteger 是线程安全的,所以在并行处理中应该不是问题。

有人可以提供示例,在哪些情况下使用这种基于状态的解决方案会出现问题?

【问题讨论】:

  • tldr: 副作用是“讨厌的”,即使是“线程安全的”。排序参数在更一般的情况下非常相关:例如。而不是添加整数(x + y == y + x),如果它是连接字符串(concat(x,y)!= concat(y,x))怎么办?如果避免了副作用,意外引入这种情况的机会就会大大减少。
  • 从文档中不清楚是需要无国籍还是只推荐无国籍。我个人认为 #1 没有问题,但我可以想象 #2 表现更好。
  • 流来自函数式编程,理想情况下应该没有副作用。有时这是不可能的,但如果有一种简单的方法可以在没有副作用的情况下实现相同的目标,那么您应该使用它。
  • 好吧,你对数字既不是顺序也不是反映源元素顺序的结果很好,唯一剩下的问题是它效率低下,比较到推荐的方法。但是大多数其他问答都是关于任务不适合这样错误的顺序,好吧,当你在这些罕见的情况下使用这种模式时,你可以接受无意义的数字,它可能很快就会成为一种习惯......

标签: java java-8 thread-safety java-stream atomicinteger


【解决方案1】:

看看 Stuart Marks here 的答案是什么 - 他正在使用有状态谓词。

这是几个潜在的问题,但如果你不关心它们或真正理解它们 - 你应该没问题。

首先是顺序,在并行处理的当前实现下展示,但如果您不关心顺序,就像在您的示例中一样,您可以。

第二个是潜在的速度AtomicInteger 将慢几倍,如前所述,增加一个简单的 int,如果你关心这个。

第三个更微妙。有时根本无法保证 map 会被执行,例如从 java-9 开始:

 someStream.map(i -> /* do something with i and numbers */)
           .count();

这里的重点是,由于您正在计数,因此无需进行映射,因此跳过了。通常,不能保证命中某些中间操作的元素到达终端之一。想象一下map.filter.map 的情况,与第二个相比,第一个映射可能“看到”更多元素,因为某些元素可能被过滤掉了。所以不建议依赖这个,除非你能准确地推断出发生了什么。

在你的例子中,IMO,你做你所做的事是非常安全的;但是如果你稍微改变你的代码,这需要额外的推理来证明它的正确性。我会选择解决方案 2,因为它对我来说更容易理解,并且没有上面列出的潜在问题。

【讨论】:

    【解决方案2】:

    另请注意,尝试从行为参数访问可变状态会使您在安全性能方面做出错误的选择;如果您不同步对该状态的访问,您就会遇到数据竞争,因此您的代码会被破坏,但是如果您确实同步对该状态的访问,您可能会因为争用而破坏您正在寻求从中受益的并行性。 最好的方法是避免有状态的行为参数来完全流式操作;通常有一种方法可以重组流管道以避免有状态。

    Package java.util.stream, Stateless behaviors

    从线程安全性和正确性的角度来看,解决方案 1 没有任何问题。但性能(作为并行处理的优势)可能会受到影响。


    为什么解决方案 #1 是不好的做法?

    我不会说这是一种不好的做法或不可接受的东西。为了性能,根本不推荐。

    他们只是提到(除非我错过了什么)“可能会出现意想不到的结果”。比如什么?

    “意外结果”是一个非常宽泛的术语,通常指不正确的同步,“到底发生了什么?”之类的行为。

    在这个例子中会发生吗?

    事实并非如此。您可能不会遇到问题。

    如果没有,你能举个例子吗?

    AtomicInteger 更改为int*,将number.incrementAndGet() 替换为++number,您将拥有一个。


    *a boxed int(例如,基于包装器、基于数组),因此您可以在 lambda 中使用它

    【讨论】:

      【解决方案3】:

      案例 2 - 在 IntStream 类的 API 注释中,通过 1 种 for 循环的增量步骤返回从 startInclusive(包含)到 endInclusive(包含)的顺序有序 IntStream,因此并行流正在逐个处理它并提供正确的顺序。

       * @param startInclusive the (inclusive) initial value
       * @param endInclusive the inclusive upper bound
       * @return a sequential {@code IntStream} for the range of {@code int}
       *         elements
       */
      public static IntStream rangeClosed(int startInclusive, int endInclusive) {
      

      案例 1 - 很明显,列表将被并行处理,因此顺序将不正确。由于映射操作是并行执行的,由于线程调度的差异,相同输入的结果可能因运行而异无法保证映射器函数如何也应用于流中的特定元素。

      Source Java Doc

      【讨论】:

      • @Kartik:不能保证在同一个流管道中对“相同”元素的不同操作在同一个线程中执行。因此无法保证映射器函数如何也应用于流中的特定元素或在哪个线程中执行任何行为参数。因此结果也可能是意外的。
      猜你喜欢
      • 1970-01-01
      • 2014-12-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多