【概念
并行流就是一个把内容分成多个数据块,并用不同的线程分别处理每一个数据块的流。
在java7之前,并行处理数据很麻烦,第一,需要明确的把包含数据的数据结构分成若干子部分。第二,给每一个子部分分配一个独立的线程。第三,
适当的时候进行同步,避免出现数据竞争带来的问题,最后将每一个子部分的结果合并。在java7中引入了forkjoin框架来完成这些步骤,而java8
中的stream接口可以让你不费吹灰之力就对数据执行并行处理,而stream接口幕后正是使用的forkjoin框架。
不过,对顺序流调用parallel()并不意味着流本身有任何的变化。它在内部实际上就是设了一个boolean标志,表示你想让parallel()之后的操作
都并行执行。类似的你可以用sequential()将并行流变为顺序流。这两个方法可以让我们更细化的控制流。
//顺序求和
public static long sum(long n){
return Stream.iterate(1l,i -> i + 1)
.limit(n)
.reduce(0l,Long::sum);
}
//并行求和
public static long parallelSum(long n){
return Stream.iterate(1l,i -> i + 1)
.limit(n)
//将流转为并行流
.parallel()
.reduce(0l,Long::sum);
}
【配置并行流线程池 并行流内部使用了默认的forkjoinPool,默认的线程数量就是处理器的数量(包括虚拟内核),通过Runtime.getRuntime().availableProcessors() 得到,我们可以使用:System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12")来改变线程池大小。【性能测试 我们不应该理所当然的任认为多线程比顺序执行的效率更高,来看下面的例子:
public class Exercise {
public static void main(String[] args) {
long num = 1000_000_0;
long st = System.currentTimeMillis();
System.out.println("iterate顺序" + sum(num) + ":" +(System.currentTimeMillis() - st));
st = System.currentTimeMillis();
System.out.println("iterate并行" + parallelSum(num) + ":" +(System.currentTimeMillis() - st));
st = System.currentTimeMillis();
System.out.println("迭代" + forSum(num) + ":" +(System.currentTimeMillis() - st));
st = System.currentTimeMillis();
System.out.println("LongStream并行" + longStreamParallelSum(num) + ":" +(System.currentTimeMillis() - st));
st = System.currentTimeMillis();
System.out.println("LongStream顺序" + longStreamSum(num) + ":" +(System.currentTimeMillis() - st));
}
//顺序求和
public static long sum(long n){
return Stream.iterate(1l,i -> i + 1)
.limit(n)
.reduce(0l,Long::sum);
}
//并行求和
public static long parallelSum(long n){
return Stream.iterate(1l,i -> i + 1)
.limit(n)
//将流转为并行流
.parallel()
.reduce(0l,Long::sum);
}
//迭代求和
public static long forSum(long n){
long result = 0;
for(long i = 0 ;i <= n ; i++){
result += i;
}
return result;
}
//longStream并行
public static long longStreamParallelSum(long n){
return LongStream.rangeClosed(1,n)
.parallel()
.reduce(0l,Long::sum);
}
//longStream顺序执行
public static long longStreamSum(long n){
return LongStream.rangeClosed(1,n)
.reduce(0l,Long::sum);
}
}
并行流执行的时间比顺序流和迭代执行的要长很多,两个原因:
1.iterate()生成的是装箱对象,必须要拆箱才能求和;
2.iterate()很难分成多个独立的块并行运行,因为每次应用这个函数都要依赖前一次的应用的结果。数字列表在归纳的过程开始时没有准备好,
因而无法有效的把流划分成小块来并行处理。但是我们又标记流为并行执行,这就给顺序执行增加了开销,每一次的求和操作都新开启了一个线程。
【使用更有针对性的的方法
LongStream.rangeClosed():
1.直接产生long类型数据,没有开箱操作
2.生成数字范围,容易拆分成独立的小块
由此可见,选择适当的数据结构往往比并行化算法更重要。并行是有代价的。并行过程需要对流做递归划分,把每个子流的操作分配到不同的线程
,然后把这些操作的结果合并成一个值。但是多核之间移动数据的代价比我们想象的要大,所以很重要的一点是保证再内核中并行执行的工作时间
比内核之间传输数据的时间要长。
【正确的使用并行流
错误使用并行流的首要原因就是使用的算法改变了共享变量的状态。如下:
这样的代码再本质上就是顺序的,因为每次访问total都会出现竞争,而使用同步方法就会是的并行毫无意义。以下是一些建议:
1.测试,并行还是顺序执行最重要的基准就是不停的测试性能。
2.留意装箱,自动装箱,拆箱会大大降低性能,java8提供了LongStream,IntStream,DoubleStream来避免这两个操作。
3.有些操作本身就是顺序执行要率高,例如:limit,findFirst等依赖元素顺序的操作。
4.当执行单个任务的成本高时使用并行,如果单个操作的成本很低,并行执行反而会因为开启线程,标记状态等操作使得效率下降。
5.小量数据不适用并行。
6.考虑流中背后的数据结构是否易于分解。ArrayList的拆分效率比LinkedList高得多,因为前者用不着便利就可以平均拆分。
另外,range工厂方法的原始类型数据流也可以快速分解。以下时流数据源的可分解性:
ArrayList:极佳
LinkedList:差
IntStream等:极佳
Stream.iterate:差
HashSet:好
TreeSet:好
7.中间操作改变流的方法,涉及到排序就不适用并行。
8.终端操作合并流的代价,涉及到排序就不适用并行。