如果可能的话,我应该总是使用平行流吗?

使用Java 8和lambdas,可以很容易地将集合作为流迭代,也可以很容易地使用并行流。的文档中的两个例子,第二个使用parallelStream:

myShapesCollection.stream()
.filter(e -> e.getColor() == Color.RED)
.forEach(e -> System.out.println(e.getName()));


myShapesCollection.parallelStream() // <-- This one uses parallel
.filter(e -> e.getColor() == Color.RED)
.forEach(e -> System.out.println(e.getName()));

只要我不关心顺序,使用并行运算总是有益的吗?有人会认为将工作分配到更多的核心上更快。

还有其他考虑吗?什么时候应该使用平行流,什么时候应该使用非平行流?

(问这个问题是为了引发关于如何以及何时使用并行流的讨论,而不是因为我认为总是使用它们是一个好主意。)

223662 次浏览

与顺序流相比,并行流具有更高的开销。协调线程需要大量的时间。默认情况下,我将使用顺序流,只有在以下情况下才考虑并行流

  • 我有大量的项目要处理(或者每个项目的处理都需要时间,并且是并行的)

  • 首先我的表现有问题

  • 我还没有在多线程环境中运行进程(例如:在web容器中,如果我已经有许多请求并行处理,在每个请求中添加一个额外的并行层可能会产生更多的负面影响而不是积极影响)

在您的示例中,无论如何性能都是由对System.out.println()的同步访问驱动的,使这个过程并行将没有任何影响,甚至是负面的影响。

此外,请记住并行流并不能神奇地解决所有的同步问题。如果进程中使用的谓词和函数使用共享资源,则必须确保所有内容都是线程安全的。特别是,如果你平行进行,副作用是你必须担心的事情。

在任何情况下,测量,不要猜测!只有测量才能告诉您并行性是否值得。

流API的设计目的是使计算的编写变得容易,这种方法可以从它们的执行方式中抽象出来,使顺序和并行之间的切换变得容易。

然而,仅仅因为它很简单,并不意味着它总是一个好主意,事实上,它是一个的想法,只是因为你可以把.parallel()放在所有地方。

首先,请注意,并行除了在有更多可用内核时可能更快地执行之外没有其他好处。并行执行总是比顺序执行涉及更多的工作,因为除了解决问题之外,它还必须执行子任务的调度和协调。希望通过将工作分散到多个处理器上,可以更快地得到答案;这种情况是否真的会发生取决于很多事情,包括数据集的大小、在每个元素上进行的计算量、计算的性质(具体来说,一个元素的处理是否与其他元素的处理相互作用?)、可用的处理器数量,以及争夺这些处理器的其他任务的数量。

此外,请注意并行性还经常暴露计算中的不确定性,而这些不确定性通常被顺序实现所隐藏;有时这并不重要,或者可以通过限制所涉及的操作来缓解(即,约简操作符必须是无状态的和关联的)。

实际上,并行有时会加快计算速度,有时不会,有时甚至会减慢计算速度。最好先使用顺序执行进行开发,然后在其中应用并行

(一),你知道,实际上有好处,提高性能和

(B),它实际上会提供更高的性能。

(一)是一个业务问题,不是技术问题。如果您是一名性能专家,您通常能够查看代码并确定(B),但明智的方法是度量。(除非你确信(一);如果代码足够快,最好把你的大脑循环应用到其他地方。)

并行最简单的性能模型是"NQ"其中N是元素的数量,Q是每个元素的计算量。通常,在开始获得性能收益之前,您需要产品NQ超过某个阈值。对于像“将1到# eyz0的数字相加”这样的低q问题,你通常会在N=1000N=10000之间看到盈亏平衡。对于高q问题,您将在较低的阈值处看到盈亏平衡。

但现实情况相当复杂。因此,在您达到专业水平之前,首先要确定顺序处理什么时候会真正让您付出代价,然后衡量并行性是否有帮助。

我看了Brian Goetz (Java语言架构师&Lambda表达式的规范引导)演讲。他详细解释了在进行并行化之前需要考虑的4点:

< p > # EYZ0 < br > -有时候分开比工作更贵!< br > # EYZ0 < br > -可以在将工作移交给另一个线程的时间内完成大量工作 # EYZ0 < br > -有时合并涉及复制大量数据。例如,添加数字是便宜的,而合并集是昂贵的 # EYZ0 < br > -房间里的大象这是每个人都可能忽略的重要一点,你应该考虑缓存缺失,如果CPU因为缓存缺失而等待数据,那么你不会通过并行化获得任何东西。这就是为什么基于数组的源并行性最好,因为下一个索引(靠近当前索引)被缓存,CPU出现缓存失败的机会更少

他还提到了一个相对简单的公式来确定并行加速的机会。

# EYZ0:

N x Q > 10000
< p > < em >, < br > N =数据项个数
Q =每一项的工作量

其他答案已经涵盖了分析,以避免过早优化和并行处理中的开销成本。这个答案解释了并行流数据结构的理想选择。

作为一个规则,并行的性能收益是最好的流在ArrayListHashMapHashSetConcurrentHashMap实例;数组;# EYZ4范围;和long范围。这些数据结构的共同之处在于,它们都可以被精确而廉价地分割成任何所需大小的子范围,这使得在并行线程之间分配工作变得很容易。streams库用来执行此任务的抽象是spliterator,它由StreamIterable上的spliterator方法返回。

所有这些数据结构共同具有的另一个重要因素是,它们在按顺序处理时提供了良好的引用位置:顺序元素引用存储在内存中。这些引用所引用的对象在内存中可能彼此不接近,这降低了引用位置。参考位置对于并行批量操作至关重要:如果没有它,线程将花费大量时间空闲,等待数据从内存传输到处理器的缓存。具有最佳引用位置的数据结构是基本数组,因为数据本身连续地存储在内存中。

来源:Joshua Bloch所著的有效Java 3e,在使流并行时要小心

永远不要让一个无限的流与一个极限并行。事情是这样的:

    public static void main(String[] args) {
// let's count to 1 in parallel
System.out.println(
IntStream.iterate(0, i -> i + 1)
.parallel()
.skip(1)
.findFirst()
.getAsInt());
}

结果

    Exception in thread "main" java.lang.OutOfMemoryError
at ...
at java.base/java.util.stream.IntPipeline.findFirst(IntPipeline.java:528)
at InfiniteTest.main(InfiniteTest.java:24)
Caused by: java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.stream.SpinedBuffer$OfInt.newArray(SpinedBuffer.java:750)
at ...

同样,如果你使用.limit(...)

< p >解释: # EYZ0 < / p >

类似地,如果流是有序的并且有比你想要处理的更多的元素,不要使用parallel。

public static void main(String[] args) {
// let's count to 1 in parallel
System.out.println(
IntStream.range(1, 1000_000_000)
.parallel()
.skip(100)
.findFirst()
.getAsInt());
}

这可能会运行更长的时间,因为并行线程可能工作在大量的数字范围上,而不是关键的0-100,这将花费很长时间。

Collection.parallelStream()是并行工作的好方法。然而,你需要记住,这有效地使用了一个公共线程池,内部只有几个工作线程(线程数默认情况下等于cpu核数),参见ForkJoinPool.commonPool()。如果池中的一些任务是长时间运行的I/ o绑定工作,那么其他的parallelStream调用(可能是快速的)将被卡住等待空闲池线程。这显然会导致fork-join任务的要求是非阻塞的和短的,换句话说,中央处理器受限。为了更好地理解细节,我强烈建议仔细阅读java.util.concurrent.ForkJoinTask javadoc,以下是一些相关引用:

ForkJoinTasks的效率源于…它们主要用作计算任务,计算纯函数或操作纯孤立的对象。

理想情况下,计算应避免同步方法或块,并应尽量减少其他阻塞同步

可细分的任务也不应该执行阻塞I/O

它们将parallelStream()任务的主要目的指示为对隔离的内存结构的简短计算。也推荐阅读文章常见的平行流陷阱