为什么不建议使用基于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的解决方案很糟糕(就像这个答案中一样),尤其是在并行流处理中(这就是我在上面使用并行流的原因,试图遇到问题)。

我查看了以下问题/答案:-
在哪些情况下,流操作应该是有状态的
在Stream中使用AtomicInteger进行索引是否合法
Java 8:计算lambda迭代次数的首选方法?

他们只是提到(除非我遗漏了什么)"可能会出现意想不到的结果"。比如什么?在这个例子中会发生这种情况吗?如果没有,你能给我举个例子吗?

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

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

有人能举例说明在使用这种基于状态的解决方案时会出现什么问题吗?

看看Stuart Marks的答案——他使用了一个有状态谓词。

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

首先是订单,在当前的并行处理实现中显示出来,但如果你不关心订单,就像在你的例子中一样,你就可以了

第二个是潜在的速度AtomicInteger将比简单的int慢几倍,正如所说,如果你关心这个的话。

第三个更微妙。有时根本不能保证map会被执行,例如因为java-9:

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

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

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

请注意,试图从行为参数访问可变状态会给您带来安全性性能方面的糟糕选择;如果不同步对该状态的访问,则存在数据争用,因此您的代码被破坏,但如果同步访问该状态,则存在争用破坏您想要从中受益的并行性的风险最好的方法是避免使用有状态的行为参数来完全流式操作;通常有一种方法可以重构流管道以避免状态。

Packagejava.util.stream,无状态行为

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


为什么解决方案#1是一种糟糕的做法?

我不会说这是一种糟糕的做法或不可接受的事情。出于性能考虑,不建议使用它。

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

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

在这个例子中会发生这种情况吗?

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

如果没有,你能给我一个发生这种情况的例子吗?

AtomicInteger更改为int*,将number.incrementAndGet()替换为++number,即可获得一个。


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

案例2-在API中,IntStream类的注释通过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-很明显,列表将并行处理,因此顺序将不正确。由于映射操作是并行执行的,由于线程调度的差异,同一输入的结果可能因运行而异,因此无法保证在同一线程中对同一流管道中的"同一"元素执行不同的操作,也无法保证映射器函数如何也应用于流中的特定元素。

源Java文档

最新更新