五、串并行流及Fork/Join框架

1.串行、并行和并发

  • 串行:执行多个任务时,各个任务按照顺序执行,执行完一个才执行下一个。
  • 并行:执行多个任务时,各个任务可以同时执行(多核CPU)。
  • 并发:执行多个任务时,各个任务被不同线程执行(单核CPU),本质上是线程抢占到时间片后执行任务,并不是同时执行多个任务。

2.思考问题

当我们需要执行一个数据量庞大的任务时,我们可以将任务分割成许多较小的任务。串行就是将这些小任务逐个按顺序完成。并行就是将这些小任务按照某种标准分配给不同的CPU内核,然后每个CPU内核的不同线程去执行各自CPU所分配的任务。哪个效率会更高呢?显然并行的方式执行更优秀。
但是线程有可能发生阻塞,导致某个CPU内核的任务无法快速执行完成,而另一个CPU内核执行完任务空闲了下来,但是任务完成的整体效率却降低了,这该如何解决?试想如果空闲的CPU内核能将其他CPU内核的任务拿来执行,这样效率会不会就大大提高了呢!
上边提到的这种方式称为“工作窃取模式”:

当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中。

Fork/Join 框架就是采用这种思想,下边我们来了解一下Fork/Join 框架。

3.Fork/Join 框架

Fork/Join 框架:就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行 join 汇总.

java串行任务转并行 java串行和并行的区别_System

相对于一般的线程池实现,fork/join框架的优势体现在对其中包含的任务的处理方式上.在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态.而在fork/join框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行.那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行.这种方式减少了线程的等待时间,提高了性能 .

使用

①继承RecursiveTask<>类,重写compute()方法(有返回值)
②继承RecursiveAction类,重写compute()方法(无返回值)

举个栗子:实现连续整数的累加

用for循环方式:

@Test
    public void test2(){
        long startTime = System.currentTimeMillis();
        long sum = 0;
        for (long i = 0; i<=100000000000L; i++) {
            sum+=i;
        }
        System.out.println(sum);
        long endTime = System.currentTimeMillis();
        System.out.println("for循环方式执行时间为:"+(endTime-startTime)+"ms");
    }

结果:

java串行任务转并行 java串行和并行的区别_临界值_02


CPU利用率:

java串行任务转并行 java串行和并行的区别_执行时间_03


用Fork/Join方式:

package com.zsr.forkandjoin;

import java.util.concurrent.RecursiveTask;

/**
 * 需求:大数的累加求和
 *
 * @author
 * @date 2019/11/6 21:35
 */
public class Sum extends RecursiveTask<Long> {

    private long begin;
    private long end;

    //进行拆分的临界值(标准)
    private static final long THRESHOLD = 10000L;


    public Sum(long begin, long end) {
        this.begin = begin;
        this.end = end;
    }

    @Override
    protected Long compute() {

        long length = end-begin;

        //如果要计算的长度小于临界值,则不再拆分直接求和,否则继续拆分直至长度小于临界值
        if (length<=THRESHOLD){
            long sum = 0;
            for (long i = begin;i <= end;i++){
                sum += i;
            }
            return sum;
        }else {
            long mid = (begin+end)/2;
            Sum left = new Sum(begin,mid);
            left.fork();//拆分子任务,压入线程队列
            Sum right = new Sum(mid+1,end);
            right.fork();

            return left.join()+right.join();
        }
    }
}
@Test
    public void test1(){
        long startTime = System.currentTimeMillis();
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask task = new Sum(0,100000000000L);
        Long result = (Long) forkJoinPool.invoke(task);
        System.out.println(result);
        long endTime = System.currentTimeMillis();
        System.out.println("ForkJoin方式执行时间为:"+(endTime-startTime)+"ms");
    }

结果:

java串行任务转并行 java串行和并行的区别_执行时间_04


CPU利用率:

java串行任务转并行 java串行和并行的区别_学习笔记_05


显然Fork/Join方式的执行效率更高,同时有效的利用了CPU的资源。但是我们也发现Fork/Join方式相较与普通方式也更加繁琐,在Java1.8中得到了优化,让程序员用一种更加便捷的对数据进行并行的操作,下边进行介绍

↓↓↓↓

4.串并行流

串行流:将某个内容的流用串行的方式进行处理。
并行流:把某个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。
Stream API 可以声明性地通过 parallel() 与sequential() 在并行流与顺序流之间进行切换。

int arr[] = {1,8,45,6,12,44};
//得到串行流
Arrays.stream(arr).sequential();
//得到并行流
Arrays.stream(arr).parallel();

依旧以“实现连续整数的累加”为例:

@Test
    public void test3(){
        long startTime = System.currentTimeMillis();
        long sum = LongStream.rangeClosed(0, 100000000000L)
                .parallel()
                .reduce(Long::sum)
                .getAsLong();
        System.out.println(sum);
        long endTime = System.currentTimeMillis();
        System.out.println("f串行流方式执行时间为:"+(endTime-startTime)+"ms");
    }

结果:

java串行任务转并行 java串行和并行的区别_System_06


CPU利用率:

java串行任务转并行 java串行和并行的区别_执行时间_07

4.总结

Stream API为我们提供了方便的对流进行切换方法,在并行流的方式下,可以更加高效和方便的进行并行操作。由于并行流的底层采用Fork/Join的方式来进行并行操作,在数据量巨大的情况下提高了CPU的利用率,大大提升了程序的执行效率。

注:菜鸟一枚,才疏学浅,希望各位前辈能够批评指正,感谢感谢!!!