1.概述

Java 8引入了流API这使得以数据流的形式遍历集合变得很容易。也很易于创建并行执行的流,并使用多个处理器核心.有人可能会认为把工作分配到更多的核上总是更快。然而,很多时候,情况并非如此。

在本教程中,我们将探讨顺序流和并行流之间的区别。我们将查看并行流使用的默认fork-join池。我们将考虑使用并行流的性能影响,包括内存局部性和拆分/合并成本。最后,我们将推荐何时将顺序流转换为并行流。

2. Java中的溪流

一种在Java中只是数据源周围的包装器,允许我们以方便的方式对数据进行批量操作。它不存储数据或对底层数据源进行任何更改。相反,它为数据管道上的功能式操作增加了支持。

2.1.连续流

默认情况下,Java中的任何流操作都是顺序处理的,除非显式指定为并行.顺序流使用单个线程来处理管道:

列表<整数> listOfNumbers =数组。asList(1, 2, 3, 4);listOfNumbers.stream()。forEach(数量- > system . out。println(number + " " + Thread.currentThread().getName()));

这个顺序流的输出是可预测的。list元素将始终按照有序序列打印:

1主要2主3主要4主要

2.2.平行流

Java中的任何流都可以很容易地从顺序转换为并行。我们可以通过添加平行方法创建顺序流或使用parallelStream集合方法

列表<整数> listOfNumbers =数组。asList(1, 2, 3, 4);listOfNumbers.parallelStream()。forEach(数量- > system . out。println(number + " " + Thread.currentThread().getName()));

并行流使我们能够在单独的核心上并行执行代码。最终结果是每个人结果的组合。但是,执行顺序超出了我们的控制权。每次运行程序时可能会发生变化:

4叉joinpool . commonpool -worker-3 2叉joinpool . commonpool -worker-5 1叉joinpool . commonpool -worker-7 3主

3. Fork-Join框架

平行流利用fork - join框架及其公共工作线程池。fork-join框架被添加到java . util . concurrent在Java 7中处理多线程之间的任务管理。

3.1.将源

fork-join框架负责在工作线程之间分割源数据,并在任务完成时处理回调

让我们看一个并行计算整数和的例子。我们将利用减少方法,并在起始和上加5,而不是从0开始:

列表<整数> listOfNumbers =数组。asList(1, 2, 3, 4);int sum = listOfNumbers.parallelStream()。减少(5、整数::总和);为了(总和).isNotEqualTo (15);

在顺序流中,此操作的结果是15。然而,由于减少操作是并行处理的,数字5实际上会在每个工作线程中加起来:

实际结果可能会因公共fork-join池中使用的线程数量而不同。为了解决这个问题,数字5应该被添加到并行流之外:

列表<整数> listOfNumbers =数组。asList(1, 2, 3, 4);int sum = listOfNumbers.parallelStream()。reduce(0, Integer::sum) + 5;为了(总和).isEqualTo (15);

因此,我们需要注意哪些操作可以并行运行。金宝搏官网188be

3.2.常见的线程池

公共池中的线程数为等于处理器的核心数。然而,API允许我们通过传递一个JVM参数来指定它将使用的线程数:

-d java.util.concurrent.forkjoinpool.common.Parlellism = 4

但是,这是一个全球设置和它会影响所有并行流和使用公共池的任何其他叉协议任务我们强烈建议不要修改这个参数,除非我们有一个很好的理由这样做。

3.3。自定义线程池

除了在默认的公共线程池中,还可以在自定义线程池

列表<整数> listOfNumbers =数组。asList(1, 2, 3, 4);ForkJoinPool customThreadPool = new ForkJoinPool(4); / /新建ForkJoinPoolint sum = customThreadPool。提交()-> listOfNumbers.parallelStream()。减少(0,整数:sum)) . get ();customThreadPool.shutdown ();为了(总和).isEqualTo (10);

再一次,Oracle推荐使用普通线程池.WE应该有一个很好的理由在自定义线程池中运行并行流。

4.性能影响

并行处理可能是有益的,可以充分利用多个核心。但是,我们还需要考虑管理多个线程,内存局部度,拆分源的开销,并合并结果。

4.1.的开销

让我们来看看一个例子整数流。我们将在顺序和并行减少操作上运行基准:

IntStream。rangeClosed(100)。减少(0, Integer::sum); IntStream.rangeClosed(1, 100).parallel().reduce(0, Integer::sum);

在这个简单的求和减少上,将顺序流转换为并行流会导致更差的性能:

基准模式CNT分数错误单位拆分件.SoursPlittingIntStreamPollellallall平行AVGT 25 35476,283±204,446 ns / op拆分。SourcePlittintTreamSequential Avgt 25 68,274±0,963 NS / OP

这背后的原因是,有时管理线程,来源和结果的开销是比实际工作更昂贵的操作

4.2.分裂的成本

均匀分割数据源是启用并行执行的必要成本。但是,一些数据源比其他数据源更好。让我们在使用一个例子上展示这一点数组列表和一个LinkedList

private static final List arrayListOfNumbers = new ArrayList<>();private static final List linkedListOfNumbers = new LinkedList<>();静态{IntStream。rangeClosed(1,1 _000_000)。forEach(i -> {arrayListOfNumbers.add(i);linkedListOfNumbers.add(我);});}

我们将运行一个基准测试,对这两种类型的列表进行顺序和并行的约简操作:

arrayListOfNumbers.stream()。减少(0, Integer::sum) arrayListOfNumbers.parallelStream().reduce(0, Integer::sum); linkedListOfNumbers.stream().reduce(0, Integer::sum); linkedListOfNumbers.parallelStream().reduce(0, Integer::sum);

我们的结果表明,将一个顺序流转换为一个并行流只会带来性能上的好处数组列表

基准模式CNT分数错误单位差异化.DOURLITCLENT.DIFFERENTSOURCEARRAYLISTPERALLEL AVGT 25 2004849,711±5289,437 NS / OP差异rouseplittle.differentsourcearrayListsequentient Avgt 25 54398,940 NS / Op差异rouseplittle.differentsourceinkedlisteDenrationallalling Avgt 25 13561609,611±275658,633 ns /OP差异验证.priting.differentsourceinkedlistseequential avgt 25 10664918,132±254251,184 ns / op

这背后的原因是阵列可以廉价而均匀地分割.另一方面,LinkedList没有这些属性。TreeMaphashset.分比LinkedList,但不如数组。

4.3.合并成本

每次我们分割源代码进行并行计算时,我们还需要确保最终将结果合并在一起。让我们在顺序和并行流上运行一个基准测试,将sum和grouping作为不同的合并操作:

arraylistofnumbers.stream()。减少(0,Integer :: Sum);arraylistofnumbers.stream()。并行()。减少(0,Integer :: Sum);arraylistofnumbers.stream()。收集(收集器.TOSET());arraylistofnumbers.stream()。并行()。收集(收集器.TOSET())

我们的结果表明,将顺序流转换为并行流只会对求和操作带来性能上的好处:

基准模式CNT得分错误单位MergingCosts.mergingCostsGroupingParallel avgt 25 135093312,675±4195024,803纳秒/运算MergingCosts.mergingCostsGroupingSequential avgt 25 70631711,489±1517217,320纳秒/运算MergingCosts.mergingCostsSumParallel avgt 25 2074483,821±7520,402纳秒/op mergingcosts.mergingcostssum sequential avgt 25 5509573,621±60249,942 ns / op

对于一些操作,如减和加,合并操作非常便宜。另一方面,合并操作,如分组到设置或映射可能是非常昂贵的

4.4。记忆位置

现代计算机使用复杂的多级高速缓存将经常使用的数据保存在处理器附近。当检测到线性内存访问模式时,硬件会预取下一行数据,假设它可能很快就会被需要。

当我们可以让处理器核心忙于做有用的工作时,并行会带来性能上的好处。由于等待缓存丢失不是有用的工作,我们需要考虑内存带宽作为一个限制因素。

让我们用两个数组的例子来演示一下,一个使用基元类型,另一个使用对象数据类型:

private static final int[] intArray = new int[1_000_000];private static final Integer[] integerArray = new Integer[1_000_000];静态{IntStream。rangeClosed(1,1 _000_000)。forEach(i -> {intArray[i-1] = i;integerArray(张)=我;});}

我们将在这两个数组上运行一个顺序和并行缩减操作的基准测试:

Arrays.stream (intArray)。减少(0,整数::总和);Arrays.stream (intArray) .parallel()。减少(0,整数::总和);Arrays.stream (integerArray)。减少(0,整数::总和);Arrays.stream (integerArray) .parallel()。减少(0,整数::总和);

我们的结果表明,当使用原语数组时,将顺序流转换为并行流会带来稍微多一点的性能好处:

基准模式CNT评分错误单位MemoryLocalityCosts.LocalityInalItArrayPoLlellallel Avgt 25 116247 787±283,150 NS / OP MockerLocalityCosts.LocityInalIntArraySequention Avgt 25 293142,385±2526,892 NS / OP MockerLocalityCosts.LocityIntioneGerarrayPoRallel Avgt 25 2153732,607±16956,463 ns / op MemoryLocalitycosts.LocalityInteGerarrayseequential AVGT 25 5134866,640±148283,942 ns / op

一系列基元带来了Java中的最佳局部性。一般来说,我们在数据结构中的指针越多,越大的压力会越高获取引用对象。这可以对并行化具有负面影响,因为多个核心同时从内存中获取数据。

4.5。的NQ模型

Oracle提供了一个简单的模型,可以帮助我们确定并行是否能提高性能。在NQ模型,N表示源数据元素的数量问:表示每个数据元素执行的计算量。

的乘积越大n *问:,我们就越有可能从并行化中获得性能提升。对于微不足道的小问题问:例如,总结数字,经验法则是N应该大于10,000。随着计算次数的增加,从并行性获得性能提升所需的数据大小减少了

5.何时使用并行流

正如我们在整个示例中所展示的,在使用并行流时,我们需要非常谨慎。在某些用例中,并行性可以带来性能方面的好处。然而,并行流不能被认为是神奇的性能助推器。因此,在开发过程中仍然应该使用顺序流作为默认值

我们可以将顺序流转换为并行流有实际的性能要求.考虑到这些需求,我们应该首先运行性能度量,并将并行性作为一种可能的优化策略。

每个元素完成的大量数据和许多计算表明并行性可能是一个很好的选择。另一方面,少量数据,不均匀的分裂源,昂贵的合并操作,并且差的存储器局部度表示并行执行的潜在问题。

6.结论

在本文中,我们探讨了Java中顺序和并行流之间的区别。我们看到并行流利用默认的Fork-Join池及其工作线程。

在示例中,我们看到并行流并不总是带来性能方面的好处。我们考虑了管理多个线程、内存局部化、分割源和合并结果的开销。我们看到数组是并行执行的一个很好的数据源,因为它们提供了可能的最佳位置,并且可以便宜而均匀地分割

最后,我们看了NQ只有当我们有实际的性能需求时,才建议建模并使用并行流。

和往常一样,源代码是可用的在GitHub

通用的底部

使用Spring 5和Spring Boot 2开始,通过学习的春天课程:

>>看看课程
客人
0.评论
内联反馈
查看所有评论