3

【概念

并行流就是一个把内容分成多个数据块,并用不同的线程分别处理每一个数据块的流。在java7之前,并行处理数据很麻烦,第一,需要明确的把包含数据的数据结构分成若干子部分。第二,给每一个子部分分配一个独立的线程。第三,适当的时候进行同步,避免出现数据竞争带来的问题,最后将每一个子部分的结果合并。在java7中引入了forkjoin框架来完成这些步骤,而java8中的stream接口可以让你不费吹灰之力就对数据执行并行处理,而stream接口幕后正是使用的forkjoin框架。不过,对顺序流调用parallel()并不意味着流本身有任何的变化。它在内部实际上就是设了一个boolean标志,表示你想让parallel()之后的操作都并行执行。类似的你可以用sequential()将并行流变为顺序流。这两个方法可以让我们更细化的控制流。

eg.java8中stream的使用:

//顺序求和
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. 生成数字范围,容易拆分成独立的小块
    

由此可见,选择适当的数据结构往往比并行化算法更重要。并行是有代价的。并行过程需要对流做递归划分,把每个子流的操作分配到不同的线程,然后把这些操作的结果合并成一个值。但是多核之间移动数据的代价比我们想象的要大,所以很重要的一点是保证再内核中并行执行的工作时间比内核之间传输数据的时间要长

【正确的使用并行流

错误使用并行流的首要原因就是使用的算法改变了共享变量的状态,因为修改共享变量意味着同步,而使用同步方法就会使的并行毫无意义。以下是一些建议:

1. 测试,并行还是顺序执行最重要的基准就是不停的测试性能。
2. 留意装箱,自动装箱,拆箱会大大降低性能,java8提供了LongStream,IntStream,DoubleStream来避免这两个操作。
3. 有些操作本身就是顺序执行要率高,例如:limit,findFirst等依赖元素顺序的操作。
4. 当执行单个任务的成本高时使用并行,如果单个操作的成本很低,并行执行反而会因为开启线程,标记状态等操作使得效率下降。
5. 小量数据不适用并行。
6. 考虑流中背后的数据结构是否易于分解。ArrayList的拆分效率比LinkedList高得多,因为前者用不着便利就可以平均拆分。另外,range工厂方法的原始类型数据流也可以快速分解。以下时流数据源的可分解性:
   - ArrayList:极佳
   - LinkedList:差
   - IntStream等:极佳
   - Stream.iterate:差
   - HashSet:好
   - TreeSet:好
7. 中间操作改变流的方法,涉及到排序就不适用并行。
8. 终端操作合并流的代价,涉及到排序就不适用并行。

【正确的使用并行

  1. 高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
  2. 并发不高、任务执行时间长的业务要区分开看:

    • 假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务
    • 假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换
  3. 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。

极品公子
221 声望43 粉丝

山不向我走来,我便向山走去。