在Java 7之前,并行处理数据集合非常麻烦。第一,你得明确地把包含数据的数据结构分成若干子部分。第二,你要给每个子部分分配一个独立的线程。第三,你需要在恰当的时候对它们进行同步来避免不希望出现的竞争条件,等待所有线程完成,最后把这些部分结果合并起来。Java 7引入了一个叫作分支/合并的框架,让这些操作更稳定、更不易出错。

Stream接口让你不用太费力气就能对数据集执行并行操作。它允许你声明性地将顺序流变为并行流。此外,你将看到Java是如何变戏法的,或者更实际地来说, 流是如何在幕后应用Java 7引入的分支/合并框架的。

1. 并行流

并行流就是一个把内容分成多个数据块,并用不同的线程分别处理每个数据块的流。

public static long sequentialSum(long n) {
return Stream.iterate(1L, i -> i + 1)
.limit(n)
.reduce(0L, Long::sum);
}
传统写法:
public static long iterativeSum(long n) {
long result = 0;
for (long i = 1L; i <= n; i++) {
result += i;
}
return result;
}

1.1 将顺序流转换为并行流

可以把流转换成并行流,从而让前面的函数归约过程(也就是求和)并行运行——对顺序流调用parallel方法:

public static long parallelSum(long n) {
return Stream.iterate(1L, i -> i + 1)
.limit(n)
.parallel()
.reduce(0L, Long::sum);
}

在现实中,对顺序流调用parallel方法并不意味着流本身有任何实际的变化。它在内部实际上就是设了一个boolean标志,表示你想让调用parallel之后进行的所有操作都并行执行。类似地,你只需要对并行流调用sequential方法就可以把它变成顺序流。请注意,你可能以为把这两个方法结合起来,就可以更细化地控制在遍历流时哪些操作要并行执行,哪些要顺序执行。

配置并行流使用的线程池

看看流的parallel方法,你可能会想,并行流用的线程是从哪来的?有多少个?怎么自定义这个过程呢?

并行流内部使用了默认的ForkJoinPool,它默认的线程数量就是你的处理器数量,这个值是由Runtime.getRuntime().available- Processors()得到的。

但是你可以通过系统属性 java.util.concurrent.ForkJoinPool.common.parallelism来改变线程池大小,如下所示:

System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");

这是一个全局设置,因此它将影响代码中所有的并行流。反过来说,目前还无法专为某个并行流指定这个值。一般而言,让ForkJoinPool的大小等于处理器数量是个不错的默认值,

除非你有很好的理由,否则我们强烈建议你不要修改它。

1.2 测量流性能

并行编程可能很复杂,有时候甚至有点违反直觉。如果用得不对(比如采用了一 个不易并行化的操作,如iterate),它甚至可能让程序的整体性能更差,所以在调用那个看似神奇的parallel操作时,了解背后到底发生了什么是很有必要的。

并行化并不是没有代价的。并行化过程本身需要对流做递归划分,把每个子流的归纳操作分配到不同的线程,然后把这些操作的结果合并成一个值。但在多个内核之间移动数据的代价也可能比你想的要大,所以很重要的一点是要保证在内核中并行执行工作的时间比在内核之间传输数据的时间长。总而言之,很多情况下不可能或不方便并行化。然而,在使用 并行Stream加速代码之前,你必须确保用得对;如果结果错了,算得快就毫无意义了。

1.3 正确使用并行流

错用并行流而产生错误的首要原因,就是使用的算法改变了某些共享状态。下面是另一种实现对前n个自然数求和的方法,但这会改变一个共享累加器:

public static long sideEffectSum(long n) {
Accumulator accumulator = new Accumulator();
LongStream.rangeClosed(1, n).forEach(accumulator::add)
return accumulator.total;
}
public class Accumulator {
public long total = 0;
public void add(long value) { total += value; }
}

这段代码本身上就是顺序的,因为每次访问total都会出现数据竞争。接下来将这段代码改为并行:

public static long sideEffectParallelSum(long n) {
Accumulator accumulator = new Accumulator();
LongStream.rangeClosed(1, n).parallel().forEach(accumulator::add);
return accumulator.total;}
System.out.println("SideEffect parallel sum done in: " + measurePerf(ParallelStreams::sideEffectParallelSum, 10_000_000L) +" msecs" );
Result: 5959989000692
Result: 7425264100768
Result: 6827235020033
Result: 7192970417739
Result: 6714157975331
Result: 7715125932481
SideEffect parallel sum done in: 49 msecs

这回方法的性能无关紧要了,唯一要紧的是每次执行都会返回不同的结果,都离正确值50000005000000差很远。这是由于多个线程在同时访问累加器,执行total += value,而这一句

《Java 8 in Action》Chapter 7:并行数据处理与性能的更多相关文章

  1. Java 8 (6) Stream 流 - 并行数据处理与性能

    在Java 7之前,并行处理集合非常麻烦.首先你要明确的把包含数据的数据结构分成若干子部分,然后你要把每个子部分分配一个独立的线程.然后,你需要在恰当的时候对他们进行同步来避免竞争,等待所有线程完成. ...

  2. 《Java 8 in Action》Chapter 4:引入流

    1. 流简介 流是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现).就现在来说,你可以把它们看成遍历数据集的高级迭代器.此外,流还可以透明地并行 ...

  3. 《Java 8 in Action》Chapter 5:使用流

    流让你从外部迭代转向内部迭代,for循环显示迭代不用再写了,流内部管理对集合数据的迭代.这种处理数据的方式很有用,因为你让Stream API管理如何处理数据.这样Stream API就可以在背后进行 ...

  4. 《Java 8 in Action》Chapter 11:CompletableFuture:组合式异步编程

    某个网站的数据来自Facebook.Twitter和Google,这就需要网站与互联网上的多个Web服务通信.可是,你并不希望因为等待某些服务的响应,阻塞应用程序的运行,浪费数十亿宝贵的CPU时钟周期 ...

  5. 《Java 8 in Action》Chapter 1:为什么要关心Java 8

    自1998年 JDK 1.0(Java 1.0) 发布以来,Java 已经受到了学生.项目经理和程序员等一大批活跃用户的欢迎.这一语言极富活力,不断被用在大大小小的项目里.从 Java 1.1(199 ...

  6. 《Java 8 in Action》Chapter 2:通过行为参数化传递代码

    你将了解行为参数化,这是Java 8非常依赖的一种软件开发模式,也是引入 Lambda表达式的主要原因.行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式.一言以蔽之,它意味 着拿出一个代码 ...

  7. 《Java 8 in Action》Chapter 3:Lambda表达式

    1. Lambda简介 可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表.函数主体.返回类型,可能还有一个可以抛出的异常列表. 匿名--我们说匿名,是因为 ...

  8. 《Java 8 in Action》Chapter 9:默认方法

    传统上,Java程序的接口是将相关方法按照约定组合到一起的方式.实现接口的类必须为接口中定义的每个方法提供一个实现,或者从父类中继承它的实现. 但是,一旦类库的设计者需要更新接口,向其中加入新的方法, ...

  9. 《Java 8 in Action》Chapter 10:用Optional取代null

    1965年,英国一位名为Tony Hoare的计算机科学家在设计ALGOL W语言时提出了null引用的想法.ALGOL W是第一批在堆上分配记录的类型语言之一.Hoare选择null引用这种方式,& ...

随机推荐

  1. Excel催化剂开源第1波-自定义函数的源代码全公开

    Excel催化剂插件从2018年1月1日开始运营,到今天刚好一周年,在过去一年时间里,感谢社区里的许多友人们的关心和鼓励,得以坚持下来,并收获一定的用户量和粉丝数和少量的经济收入回报和个人知名度的提升 ...

  2. vijos P1911 珠心算测验

    #include<iostream> using namespace std; int main() { int n; int sum = 0; int num[200]; cin > ...

  3. [OpenGL] 绘制并且判断凹凸多边形、自相交多边形。

    #include <iostream> #include <ctime> #include <GL/glut.h> #include <math.h> ...

  4. Apache Tomcat 绿色版安装Service(服务)

    1.配置CATALINA_HOME的环境变量:  变量名:CATALINA_HOME  值:tomcat安装或解压的根目录如:c:\Apache tomcat6.0 2.开始->运行->c ...

  5. linux初学者-常用基本命令篇

    linux系统中有着许许多多的命令,并且软件也有可能自带命令,要想全部了解这些命令是很困难的,但是有一些基本命令是在平时的学习工作中应用的很广泛的.以下简要介绍几种linux系统中的常用命令. 1.m ...

  6. leetcode 48 矩阵旋转可以这么简单

    一行代码解决矩阵旋转(方法三). 方法1: 坐标法 def rotate(self, matrix): n = len(matrix) # 求出矩阵长度 m = (n + 1) // 2 # 求出层数 ...

  7. KNN算法实现手写体区分

    KNN算法在python里面可以使用pip install指令安装,我在实现之前查看过安装的KNN算法,十分全面,包括了对于手写体数据集的处理.我这里只是实现了基础的识别方法,能力有限,没有数据处理方 ...

  8. 箭头函数=>

    箭头函数特点一:没有自己的this对象,其this对象为所在环境 特点二 :没有arguments参数 可以用...rest代替特点三:不能使用构造函数,不可使用new命令,否则会报错 //函数的扩展 ...

  9. bug探索常识

    1.什么是Bug探索测试? 探索测试是将测试设计和测试执行整合在一起,形成的一种测试方法. 2.探索性测试怎么做? 需要认真思考和分析结果,并且在探索测试的过程中做记录. 3.探索性测试的好处? 可以 ...

  10. ceph log机制

    Log 是每个项目必须的,他是跟踪问题的最直接的依据.Ceph 也设计自己的log机制. 初始化启动log实例,启动log线程. _log = new ceph::log::Log(&_con ...