7

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

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基于解决方案不好(比如在这个答案中),特别是在并行流处理中(这就是我在上面使用并行流来尝试遇到问题的原因)。

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

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

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

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

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

4

3 回答 3

4

好吧,看看 Stuart Marks 的答案-他正在使用有状态的谓词。

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

首先是顺序,在当前的并行处理实现下展示,但如果你不关心顺序,就像你的例子一样,你没问题。

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

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

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

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

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

于 2018-11-16T04:50:53.573 回答
2

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

java.util.stream,无状态行为

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


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

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

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

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

在这个例子中会发生吗?

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

如果没有,你能给我一个可能发生的例子吗?

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


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

于 2018-11-16T05:33:16.917 回答
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 - 很明显,列表将被并行处理,因此顺序将不正确。由于映射操作是并行执行的,由于线程调度的差异,相同输入的结果可能因运行而异无法保证映射器函数如何也应用于流中的特定元素。

源 Java 文档

于 2018-11-16T03:49:34.150 回答