线程(十三)---ForkJoin

写在前面:各位看到此博客的小伙伴,如有不对的地方请及时通过私信我或者评论此博客的方式指出,以免误人子弟。多谢!

  到这里,关于 CompletableFuture 的基本使用你已经了解的差不多了,不知道你是否注意,我们前面说的带有 Sync 的方法是单独起一个线程来执行,但是我们并没有创建线程,这是怎么实现的呢?细心的朋友如果仔细看每个变种函数的第三个方法也许会发现里面都有一个 Executor 类型的参数,用于指定线程池,因为实际业务中我们是严禁手动创建线程的,如果没有指定线程池,那自然就会有一个默认的线程池,也就是 ForkJoinPool。

  ForkJoinPool 的线程数默认是 CPU 的核心数。但是,最好不要所有业务共用一个线程池,因为,一旦有任务执行一些很慢的 I/O 操作,就会导致线程池中所有线程都阻塞在 I/O 操作上,从而造成线程饥饿,进而影响整个系统的性能。

  什么是fork/join呢,它的原理是啥,在这整理记录一把。

使用Fork/Join介绍

简单的说ForkJoin就是在必要的情况下,将一个大任务,进行拆分成若干个小任务(拆到不可再拆时),再将一个个小的任务运算的结果进行jion汇总。通过网上的一张图来理解Fork/Join,

在这里插入图片描述

 

使用Fork/Join框架

1.继承RecursiveTask或者RecursiveAction,重写compute()方法。RecursiveTask有返回值,RecursiveAction没有返回值

2.要执行一个ForkJoin的任务,首先建一个线程池ForkJoinPool,使用线程池运行任务。

3.使用fork方法拆分任务。

4.使用ForkJoinPool调用invoke方法来执行一个任务。

5.join合并计算结果。

为了对比下使用fork/join和普通循环在效率方面的区别,再新增一个循环计算的方法,代码示例如下:

public static void main(String[] args) {
        userForLoop();
        long startTime = System.currentTimeMillis();
        ForkJoinPool pool = new ForkJoinPool();
        ForkJoinClass task = new ForkJoinClass(0,100000L);// 10000000000L
        long sum = pool.invoke(task);
        System.out.println(sum);
        long endTime = System.currentTimeMillis();
        System.out.println("使用fork/join耗时:" + (endTime - startTime));
    }

   static class ForkJoinClass extends RecursiveTask<Long>{

       private long start;
       private long end;
       private static final long threshold = 200L;

       public ForkJoinClass(long start,long end){
           this.start = start;
           this.end = end;
       }

       @Override
       protected Long compute() {
           long length = end - start;
           if(length <= threshold){
               System.out.println("开始计算:startValue = " + start + ";endValue = " + end);
               long sum = 0;
               for(long i = start; i <= end; i++){
                   sum += i;
               }
               return sum;
           }else {
               long middle = (start + end)/2;
               ForkJoinClass left = new ForkJoinClass(start,middle);
               left.fork(); // 拆分 并将该子任务压入线程队列
               ForkJoinClass right = new ForkJoinClass(middle + 1,end);
               right.fork();
               return left.join() + right.join();
           }
       }
   }

   static void userForLoop(){
       long startTime = System.currentTimeMillis();
       long sum = 0L;
       for(long i = 0; i <= 100000L; i++){
           sum += i;
       }
       System.out.println(sum);
       long endTime = System.currentTimeMillis();
       System.out.println("使用普通循环耗时:" + (endTime - startTime));
   }

执行结果如下:

 

从执行结果上看,普通循环比使用fork/join还要快,forkjoin拆分合并任务也是需要时间的,对于计算量比较小的任务,拆分合并所花费的时间可能会大于计算时间,这时候用forkjoin拆分任务就会有点得不偿失了,如果将需要计算的结果增大到10000000000L就可以看出使用fork/join耗时明显小于普通循环。

注意:使用ForkJoin时,任务的量一定要大,否则太小,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工作窃取模式

  先通过一个图理解工作窃取模式,如下:

在这里插入图片描述

如上图,工作线程2,3,4已经执行完任务处于空闲状态,为了方便理解,任务以数字表示工作任务量,工作线程1还有1个任务量为4的任务要执行,fork为两个任务量为2的任务,这时候工作线程2窃取一个任务,这时线程1,2分别有一个工作量为2的任务,继续拆分后,分别拆分为两个任务量为1的任务,分别被工作线程3和4窃取一个任务,其实最后线程1,2,3,4分别执行了一个任务,理论上除去fork,join操作的时间,效率会提高4倍。

 
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值