jdk8的新特性总结(三):串行流与并行流

在上一篇文章中我们知道通过parallelStream方法可以获得一个并行流,那么什么是并行流呢?并行流就是把内容分割成多个数据块,每个数据块对应一个流,然后用多个线程分别处理每个数据块中的流。

java8中将并行进行了优化,我们可以很容易的对数据进行并行操作,Stream API可以声明式的通过paralleleStream和sequential方法在并行流和顺序流之间进行切换。

一、Fork/Join框架

在必要的条件下,将一个大任务进行拆分Fork,拆分成若干个小任务(拆到不可再拆时),再将若干个小任务的计算结果进行Join汇总。

forkjoin框架图

二、Fork/Join框架与传统线程池的区别?

ForkJoin框架采用的是“工作窃取模式”,传统线程在处理任务时,假设有一个大任务被分解成了20个小任务,并由四个线程A,B,C,D处理,理论上来讲一个线程处理5个任务,每个线程的任务都放在一个队列中,当B,C,D的任务都处理完了,而A因为某些原因阻塞在了第二个小任务上,那么B,C,D都需要等待A处理完成,此时A处理完第二个任务后还有三个任务需要处理,可想而知,这样CPU的利用率很低。而ForkJoin采取的模式是,当B,C,D都处理完了,而A还阻塞在第二个任务时,B会从A的任务队列的末尾偷取一个任务过来自己处理,C和D也会从A的任务队列的末尾偷一个任务,这样就相当于B,C,D额外帮A分担了一些任务,提高了CPU的利用率。

三、Fork/Join框架代码示例:

首先要编写一个ForkJoin的计算类继承RecursiveTask<T> 并重写  T compute() 方法

/**
 * forkjoin框架使用示例: 利用ForkJoin框架求一个区间段的和
 */
public class ForkJoinTest extends RecursiveTask<Long> {
    private static final long serialVersionUID = 123134564L;
    //计算的起始值
    private Long start;
    //计算的终止值
    private Long end;
    //做任务拆分时的临界值
    private static final Long THRESHOLD = 10000L;

    public ForkJoinTest(Long start, Long end) {
        this.start = start;
        this.end = end;
    }

    /**
     * 计算代码,当计算区间的长度大于临界值时,继续拆分,当小于临界值时,进行计算
     *
     * @return
     */
    @Override
    protected Long compute() {
        Long length = this.end - this.start;
        if (length > THRESHOLD) {
            long middle = (start + end) / 2;
            ForkJoinTest left = new ForkJoinTest(start, middle);
            left.fork();
            ForkJoinTest right = new ForkJoinTest(middle + 1, this.end);
            right.fork();
            return left.join() + right.join();
        } else {
            long sum = 0;
            for (long i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        }
    }
}

然后我们写一个测试类,来测试一下ForkJoin框架

public class TestForkJoin {
	
	@Test
    public void test1(){
        long start = System.currentTimeMillis();
        //1.ForkJoin框架也需要一个ForkJoin池来启动
        ForkJoinPool pool = new ForkJoinPool();
        //2.创建一个ForkJoinTask,RecursiveTask也是继承自ForkJoinTask,所以我们new自己写的那个计算类
        ForkJoinTask<Long> task = new ForkJoinTest(0L, 100000000000L);
        //3.执行计算
        long sum = pool.invoke(task);
        System.out.println(sum);

        long end = System.currentTimeMillis();

        System.out.println("耗费的时间为: " + (end - start)); //5463
    }
	/**
     * 测试用for循环计算0到1一亿的和
     */
	@Test
	public void test2(){
		long start = System.currentTimeMillis();
		long sum = 0L;
		for (long i = 0L; i <= 10000000000L; i++) {
			sum += i;
		}
		System.out.println(sum);
		long end = System.currentTimeMillis();
		System.out.println("耗费的时间为: " + (end - start)); //7610
	}
	/**
     * 测试用并行流计算0到1一亿的和
     */
	@Test
	public void test3(){
		long start = System.currentTimeMillis();
		Long sum = LongStream.rangeClosed(0L, 10000000000L).parallel().sum();
		System.out.println(sum);
		long end = System.currentTimeMillis();
		System.out.println("耗费的时间为: " + (end - start)); //2813
	}

}

通过上面的比较,我们发现并行流的处理效率是比较高的,不过并行流底层也是使用的forkjoin框架,只是java8底层已经实现好了,forkjoin拆分合并任务也是需要时间的,对于计算量比较小的任务,拆分合并所花费的时间可能会大于计算时间,这时候用forkjoin拆分任务就会有点得不偿失了。

总结:

1、使用parallelStream方法可以得到一个并行流,并行流底层使用的是forkjoin框架,对于一些计算量比较大的任务,使用并行流可能极大的提升效率。

2、ForkJoin框架的使用方式,

  • 编写计算类继承RecursiveTask<T>接口并重写T compute方法;
  • 使用fork方法拆分任务,join合并计算结果;
  • 使用ForkJoinPool调用invoke方法来执行一个任务。

PS:上面练习的代码放在了我的git仓库中,感兴趣的可以down下来再练练:https://github.com/caishi13202/jdk8.git

猜你喜欢

转载自blog.csdn.net/caishi13202/article/details/82667230