我应该尽可能使用并行流吗?

时间:2013-12-04 12:17:46

标签: java parallel-processing java-8 java-stream

使用Java 8和lambdas,可以很容易地将集合作为流进行迭代,同样易于使用并行流。来自the docs的两个示例,第二个使用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()));

只要我不关心顺序,使用并行是否总是有益的?人们会认为将更多核心的工作划分得更快。

还有其他考虑因素吗?什么时候应该使用并行流?什么时候应该使用非并行?

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

6 个答案:

答案 0 :(得分:590)

与顺序流相比,并行流具有更高的开销。协调线程需要花费大量时间。我默认使用顺序流,如果

,只考虑并行流
  • 我需要处理大量的项目(或者每个项目的处理需要时间并且可以并行化)

  • 我首先遇到了性能问题

  • 我还没有在多线程环境中运行该进程(例如:在Web容器中,如果我已经有很多请求并行处理,在每个请求中添加一个额外的并行层可以

在您的示例中,无论如何,性能将由System.out.println()的同步访问驱动,并且使此过程并行将不起作用,甚至是否定的。

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

无论如何,衡量,不要猜!只有衡量标准才会告诉您并行性是否值得。

答案 1 :(得分:202)

Stream API旨在使编写计算变得容易,这种方式从执行方式中抽象出来,使顺序和并行之间的切换变得容易。

然而,仅仅因为它简单,并不意味着它总是一个好主意,事实上,仅仅因为放弃.parallel()所有这一切都是糟糕的想法您可以。

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

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

实际上,有时并行性会加速你的计算,有时它不会,有时它甚至会减慢它的速度。最好先使用顺序执行开发,然后应用并行性,其中(A)您知道实际上有益于提高性能,(B)它实际上会提高性能。 (A)是业务问题,而不是技术问题。如果您是性能专家,您通常可以查看代码并确定(B),但智能路径是衡量。 (而且,在你确信(A)之前不要打扰;如果代码足够快,最好在其他地方应用你的大脑周期。)

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

但实际情况相当复杂。因此,在您获得专业知识之前,首先要确定顺序处理何时实际上是在为您付出代价,然后衡量并行性是否会有所帮助。

答案 2 :(得分:49)

我观看了{strong> Brian Goetz presentations之一(Lambda Expressions的Java语言架构师和规范主管)。他在进行并行化之前详细解释了以下4个要点:

拆分/分解成本
- 有时分裂比仅仅做工作更昂贵!
任务调度/管理成本
- 在将工作交给另一个线程所花费的时间内可以做很多工作 结果合并费用
- 有时组合涉及复制大量数据。例如,添加数字很便宜,而合并集则很昂贵 的局部性
- 房间里的大象。这是每个人都可能错过的重点。您应该考虑缓存未命中,如果CPU由于缓存未命中而等待数据,那么您将无法通过并行获得任何内容。这就是为什么基于数组的源并行化最佳,因为下一个索引(在当前索引附近)被缓存,并且CPU遇到缓存未命中的可能性更小。

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

NQ模型

N x Q > 10000

其中,
N =数据项数量
Q =每件商品的工作量

答案 3 :(得分:11)

JB击中了头部的钉子。我唯一可以添加的是Java8不进行纯并行处理,它确实paraquential是的我写了这篇文章而且我已经做了三十年的F / J所以我确实理解了这个问题。

答案 4 :(得分:0)

其他答案已经涵盖了性能分析,以避免过早优化和并行处理中的间接费用。这个答案说明了并行流数据结构的理想选择。

  

通常,在ArrayListHashMapHashSetConcurrentHashMap实例上的流上,并行性带来的性能提升最佳;数组; int范围;和long范围。这些数据结构的共同之处在于,它们都可以准确而便宜地拆分为任意大小的子范围,这使得在并行线程之间轻松进行工作分配变得容易。流库用来执行此任务的抽象是分隔符,它由spliteratorStream上的Iterable方法返回。

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

来源:Item#48 Joshua Bloch编写并行并行流,有效Java 3e时要小心

答案 5 :(得分:0)

永远不要并行化具有限制的无限流。这是发生了什么:

    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(...)

相同

此处说明: Java 8, using .parallel in a stream causes OOM error

类似地,例如,如果流是有序的并且具有比您要处理的元素更多的元素,则不要使用并行操作,例如

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,这会花费很长时间。