最近看了网上的某公开课,其中有讲到forkjoin框架。在这以前,我丝毫没据说过这个东西,很好奇是什么东东。因而,就顺道研究了一番。程序员
总感受这个东西,用的地方不多,也有多是我才疏学浅。好吧,反正问了身边一堆猿,没有一个知道的。多线程
所以,我也没有那么深刻的去了解底层,只是大概的了解了其工做原理,并分析了下它和普通的for循环以及JDK8的stream流之间的性能对比(稍后会说明其中踩到的坑)。框架
1、forkjoin介绍ide
forkjoin是JDK7提供的并行执行任务的框架。 并行怎么理解呢,就是能够充分利用多核CPU的计算能力,让多个CPU同时进行任务的执行,从而使单位时间内执行的任务数尽可能多,所以表现上就提升了执行效率。oop
它的主要思想就是,先把任务拆分红一个个小任务,而后再把全部任务汇总起来,简而言之就是分而治之。若是你了解过hadoop的MapReduce,就能理解这种思想了。不了解也不要紧,下面画一张图,你就能明白了。性能
上边的任务拆分为多个子任务的过程就是fork,下边结果的归并操做就是join。(注意子任务和多线程不是一个概念,而是一个线程下会有多个子任务)测试
另外,forkjoin有一个工做窃取的概念。简单理解,就是一个工做线程下会维护一个包含多个子任务的双端队列。而对于每一个工做线程来讲,会从头部到尾部依次执行任务。这时,总会有一些线程执行的速度较快,很快就把全部任务消耗完了。那这个时候怎么办呢,总不能空等着吧,多浪费资源啊。this
因而,先作完任务的工做线程会从其余未完成任务的线程尾部依次获取任务去执行。这样就能够充分利用CPU的资源。这个很是好理解,就好比有个妹子程序员作任务比较慢,那么其余猿就能够帮她分担一些任务,这简直是共赢的局面啊,妹子开心了,你也开心了。线程
2、实操测试性能3d
话很少说,先上代码,计算的是从0加到10亿的结果。
public class ForkJoinWork extends RecursiveTask<Long> { private long start; private long end; //临界点 private static final long THRESHOLD = 1_0000L; public ForkJoinWork(long start, long end) { this.start = start; this.end = end; } @Override protected Long compute() { long len = end - start; //不大于临界值直接计算结果 if(len < THRESHOLD){ long sum = 0L; for (long i = start; i <= end; i++) { sum += i; } return sum; }else{ //大于临界值时,拆分为两个子任务 Long mid = (start + end) /2; ForkJoinWork task1 = new ForkJoinWork(start,mid); ForkJoinWork task2 = new ForkJoinWork(mid+1,end); task1.fork(); task2.fork(); //合并计算 return task1.join() + task2.join(); } } } public class ForkJoinTest { public static void main(String[] args) throws Exception{ long start = 0L; long end = 10_0000_0000L; testSum(start,end); testForkJoin(start,end); testStream(start,end); } /** * 普通for循环 - 1273ms * @param start * @param end */ public static void testSum(Long start,Long end){ long l = System.currentTimeMillis(); long sum = 0L; for (long i = start; i <= end ; i++) { sum += i; } long l1 = System.currentTimeMillis(); System.out.println("普通for循环结果:"+sum+",耗时:"+(l1-l)); } /** * forkjoin方式 - 917ms * @param start * @param end * @throws Exception */ public static void testForkJoin(long start,long end) throws Exception{ long l = System.currentTimeMillis(); ForkJoinPool forkJoinPool = new ForkJoinPool(); ForkJoinWork task = new ForkJoinWork(start,end); long invoke = forkJoinPool.invoke(task); long l1 = System.currentTimeMillis(); System.out.println("forkjoin结果:"+invoke+",耗时:"+(l1-l)); } /** * stream流 - 676ms * @param start * @param end */ public static void testStream(Long start,Long end){ long l = System.currentTimeMillis(); long reduce = LongStream.rangeClosed(start, end).parallel().reduce(0, (x, y) -> x + y); long l1 = System.currentTimeMillis(); System.out.println("stream流结果:"+reduce+",耗时:"+(l1-l)); } }
这里解释下,首先咱们须要建立一个ForkJoinTask,自定义一个类来继承ForkJoinTask的子类RecursiveTask,这是为了拿到返回值。另外还有一个子类RecursiveAction是不带返回值的,这里咱们暂时用不到。
而后,须要建立一个ForkJoinPool来执行task,最后调用invoke方法来获取最终执行的结果。它还有两种执行方式,execute和submit。这里不展开,感兴趣的能够自行查看源码。
铛铛,重点来了。
我测试了下比较传统的普通for循环,来对比forkjoin的执行速度。计算的是从0加到10亿,在个人win7电脑上确实是forkjoin计算速度快。这时,坑来了,一样的代码,没有任何改动,我搬到mac电脑上,计算结果却大大超出个人意外——forkjoin居然比for循环慢了一倍,对的没错,执行时间是for循环的二倍。
这就让我特别头大了,这究竟是什么缘由呢。通过屡次测试,终于搞明白了。forkjoin这个框架针对的是大任务执行,效率才会明显的看出来有提高,因而我把总数调大到20亿。
另外还有个关键点,经过设置不一样的临界点值,会有不一样的结果。逐渐的加大临界点值,效率会进一步提高。好比,我分别把THRESHOLD设置为1万,10万和100万,执行时间会逐步缩短,而且会比for循环时间短。感兴趣的,可本身手动操做一下,感觉这个微妙的变化。
所以,最终修改成从0加到20亿,临界值设置为100万,就出现了如下结果:
普通for循环结果:2000000001000000000,耗时:1273 forkjoin结果:2000000001000000000,耗时:917 stream流结果:2000000001000000000,耗时:676
能够明显看出来,forkjoin确实是比for循环快的。固然,逐步的再加大总数到100亿或者更大,而后调整合适的临界值,这种对比会更加明显。(就是心疼电脑会冒烟,不敢这样测试)
最后,说下JDK8提供的Stream流计算,能够看到,这个计算速度是三种方式中最快的。奈你forkjoin再牛逼,一般仍是比不过Stream的,从这个方法parallel的名字就看出来,也是并行计算。因此,这也是我感受forkjoin好像没什么存在感的缘由,Stream不香吗。(固然,也有多是forkjoin还有更牛逼的功能待我去发掘。)