为什么不建议使用基于AtomicInteger的流解决方案?

时间:2018-11-16 00:37:34

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

说我有以下水果清单:-

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是线程安全的,因此在并行处理中应该不是问题。

有人可以提供使用这种基于状态的解决方案时会出现问题的示例吗?

3 个答案:

答案 0 :(得分:3)

好好看看Stuart Marks here的答案-他使用的是有状态谓词。

这是两个潜在的问题,但是如果您不关心它们或不真正了解它们,则应该没事。

首先是顺序,在当前实现中针对并行处理显示,但是如果您不关心顺序(例如您的示例),就可以了。

第二个是潜在的速度AtomicInteger,要增加一个简单的int的速度将慢很多,如您所说,

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

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

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

在您的示例IMO中,您做自己做的事绝对安全;但是,如果您稍稍更改代码,则需要进行其他推理以证明其正确性。我会选择解决方案2,只是因为它对我来说更容易理解,并且没有上面列出的潜在问题。

答案 1 :(得分:2)

  

还请注意,尝试从行为参数访问可变状态会给您带来安全性能的错误选择;如果您不同步对该状态的访问,则将导致数据争用,因此代码将被破坏,但是如果您确实同步对该状态的访问,则可能会面临争用破坏寻求从中受益的并行性。 / strong>最好的方法是避免使用有状态的行为参数来完全流式传输操作;通常,有一种方法可以重组流管道以避免状态化。

     

Package java.util.stream, Stateless behaviors

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


  

为什么解决方案#1不好?

我不会说这是一个不好的做法或不可接受的事情。出于性能考虑,不建议这样做。

  

他们只是提到(除非我错过了一些事情)“可能会发生意外的结果”。喜欢什么?

“意想不到的结果”是一个非常宽泛的术语,通常是指不正确的同步,例如“到底发生了什么事?”。

  

在这个例子中会发生吗?

不是这样。您可能不会遇到问题。

  

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

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


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

答案 2 :(得分:1)

案例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