在Java8中,编写并行化的程序很容易。
从外部迭代到内部迭代的过程,让编写简洁的代码更加容易,也让程序员不需要手动控制迭代过程了。
并行和并发
并发是两个任务共享时间段,并行则是两个任务在同一时间发生,比如运行在多核CPU上。如果一个程序要运行两个任务,并且只有一个CPU给它们分配了不同的时间片,那么这就是并发,而不是并行。

数据并行化是指讲数据分成块,为每块数据分配单独的处理单元。
当需要在大量数据上执行同样的操作时,数据并行化很管用。它将问题分解为可在多块数据上求解的形式,然后对每块数据执行运算,最后将各数据块上得到的结果汇总,从而得到最终答案。
另一个相对应的概念是“任务并行化”,在任务并行化中,线程不同,工作各异。
为什么并行化如此重要
过去我们可以之指望CPU时钟频率会变得越来越快,然而在过去的十年中,主流芯片厂商转向了多核处理器。我们不能再依赖提升CPU时钟频率来提高现有代码的计算能力,需要利用现代CPU的架构,而这唯一的办法就是编写并行化代码。
并行化流操作
并行化操作只需要改变一个方法的调用。如果已经有一个Stream对象,调用它的parallel方法就能让其拥有并行操作的能力。如果想从一个集合类创建一个流,调用parallelStream就能立即获得一个拥有并行能力的流。
串行化计算专辑曲目长度:
public int serialArraySum() {
return albums.stream()
.flatMap(Album::getTracks)
.mapToInt(Track::getLength)
.sum();
}
并行化计算专辑曲目长度:
public int parallelArraySum() {
return albums.parallelStream()
.flatMap(Album::getTracks)
.mapToInt(Track::getLength)
.sum();
}
但是,不要急于将手上代码中的stream方法全部修改为parallelStream方法。因为并行化运行基于流的代码不一定比串行化运行更快。
以上边的例子说明:再一个四和电脑上,如果由10张专辑,串行化代码的速度是并行化代码速度的8倍;如果将专辑数量增至100张,串行化和并行化速度相当;如果将专辑数量增至10000张,则并行化代码的速度是串行化代码速度的2.5倍。
输入流的大小并不是决定并行化是否会带来速度提升的唯一因素,性能还会受到编写代码的方式和核的数量的影响。
模拟系统
并行化流操作的用武之地是使用简单操作处理大量数据。
限制
reduce操作的一个显示是组合操作必须符合结合律。这意味着只要序列的值不变,组合操作的顺序不重要。
性能
影响并行流性能的主要因素由5个:
数据大小
输入数据的大小会影响并行化处理对型嫩的提升。将问题分解之后并行化处理,再将结果合并会带来额外的开销。因此,只有数据足够大,每个数据处理管道花费的时间足够多时,并行化处理才由意义。
源数据结构
每个管道的操作都基于一些初始数据源,通常是集合。将不同的数据源分割相对容易,这里的开销影响了再管道中并行处理数据时到底能带来多少性能上的提升。
装箱
处理基本类型比处理装箱类型要快
核的数量
极端情况下,只有一个核,因此完全没有必要并行化。显然,拥有的核越多,获得潜在性能提升的幅度就越大。再实践中,核的数量不单指你的机器上有多少核,更是指运行时你的机器能使用多少核。也就是说同时运行的其他进程,或者线程关联性(强制线程再某些核或CPU上运行)会影响性能。
单元处理开销
比如数据大小,这是一场并行执行花费时间和分解合并操作开销之间的战争。花在流中每个元素身上的时间越长,并行操作带来的性能提升越明显。
在底层,并行流还是沿用了fork/join框架。fork递归式地分解问题,然后每段并行执行,最终由join合并结果,返回最后的值。

我们可以根据性能的好坏,将核心类库提供的通用数据结构分成以下3 组。
- 性能好
ArrayList、数组或IntStream.range,这些数据结构支持随机读取,也就是说它们能轻而易举地被任意分解。 - 性能一般
HashSet、TreeSet,这些数据结构不易公平地被分解,但是大多数时候分解是可能的。 - 性能差
有些数据结构难于分解,比如,可能要花O(N) 的时间复杂度来分解问题。其中包括LinkedList,对半分解太难了。还有Streams.iterate 和BufferedReader.lines,它们长度未知,因此很难预测该在哪里分解。
初始的数据结构影响巨大。举一个极端的例子,对比对10 000 个整数并行求和,使用ArrayList要比使用LinkedList 快10 倍。这不是说业务逻辑的性能情况也会如此,只是说明了数据结构对于性能的影响之大。使用形如LinkedList 这样难于分解的数据结构并行运行可能更慢。
理想情况下,一旦流框架将问题分解成小块,就可以在每个线程里单独处理每一小块,线程之间不再需要进一步通信。无奈现实不总遂人愿!
在讨论流中单独操作每一块的种类时,可以分成两种不同的操作:无状态的和有状态的。无状态操作整个过程中不必维护状态,有状态操作则有维护状态所需的开销和限制。
如果能避开有状态,选用无状态操作,就能获得更好的并行性能。无状态操作包括map、filter 和flatMap,有状态操作包括sorted、distinct 和limit。
并行化数组操作

使用并行化数组操作初始化数组
public static double[] parallelInitialize(int size) {
double[] values = new double[size];
Arrays.parallelSetAll(values, i -> i);
return values;
}
计算简单滑动平均数
public static double[] simpleMovingAverage(double[] values, int n) {
double[] sums = Arrays.copyOf(values, values.length);
Arrays.parallelPrefix(sums, Double::sum);
int start = n - 1;
return IntStream.range(start, sums.length)
.mapToDouble(i -> {
double prefix = i == start ? 0 : sums[i - n];
return (sums[i] - prefix) / n;
})
.toArray();
}
网友评论