java并发编程学习5--并行流

【概念

    并行流就是一个把内容分成多个数据块,并用不同的线程分别处理每一个数据块的流。
    在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.终端操作合并流的代价,涉及到排序就不适用并行。




你可能感兴趣的:(学习)