说我有以下水果清单:-
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
是线程安全的,因此在并行处理中应该不是问题。
有人可以提供使用这种基于状态的解决方案时会出现问题的示例吗?
答案 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>最好的方法是避免使用有状态的行为参数来完全流式传输操作;通常,有一种方法可以重组流管道以避免状态化。
从线程安全性和正确性的角度来看,解决方案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-很明显,列表将被并行处理,因此顺序将不正确。由于映射操作是并行执行的,由于线程调度的差异,同一输入的结果可能因运行而异,因此不能保证在同一线程中对同一流管道中“相同”元素的不同操作也要在同一线程中执行。无法保证将映射器函数也应用于流中的特定元素。