上面的例子是无返回值的案例,下面我们来看一个典型的有返回值的案例,相信大家都听过及很熟悉斐波那契数列,这个数列有个特点就是最后一项的结果等于前两项的和,如: 0,1,1,2,3,5...f(n-2)+f(n-1), 即第0项为0 ,第一项为1,则第二项为 0+1=1,以此类推。我们最初的解决方法就是使用递归来解决,如下计算第n项的数值:
private int num(int num){ if (num <= 1){ return num; } num = num(num-1) + num(num -2); return num; }从上面简单代码中可以看到,当 n<=1 时返回 n , 如果n>1 则计算前一项的值f1,在计算前两项的值f2, 再将两者相加得到结果,这就是典型的递归问题,也是对应我们的ForkJoin 的工作模式,如下所示,根节点产生子任务,子任务再次衍生出子子任务,到最后在进行整合汇聚,得到结果。
我们通过 ForkJoinPool 来实现斐波那契数列的计算,如下展示:
/** * @url: i-code.online * @author: AnonyStar * @time: 2020/11/2 10:01 */ public class ForkJoinApp3 { public static void main(String[] args) throws ExecutionException, InterruptedException { ForkJoinPool pool = new ForkJoinPool(); //计算第二是项的数值 final ForkJoinTask<Integer> submit = pool.submit(new Fibonacci(20)); // 获取结果,这里获取的就是异步任务的最终结果 System.out.println(submit.get()); } } class Fibonacci extends RecursiveTask<Integer>{ int num; public Fibonacci(int num){ this.num = num; } @Override protected Integer compute() { if (num <= 1) return num; //创建子任务 Fibonacci subTask1 = new Fibonacci(num - 1); Fibonacci subTask2 = new Fibonacci(num - 2); // 执行子任务 subTask1.fork(); subTask2.fork(); //获取前两项的结果来计算和 return subTask1.join()+subTask2.join(); } }
通过 ForkJoinPool 可以极大的发挥多核处理器的优势,尤其非常适合用于递归的场景,例如树的遍历、最优路径搜索等场景。
上面说的是ForkJoinPool 的使用上的,下面我们来说一下其内部的构造,对于我们前面说的几种线程池来说,它们都是里面只有一个队列,所有的线程共享一个。但是在ForkJoinPool 中,其内部有一个共享的任务队列,除此之外每个线程都有一个对应的双端队列Deque , 当一个线程中任务被Fork 分裂了,那么分裂出来的子任务就会放入到对应的线程自己的Deque中,而不是放入公共队列。这样对于每个线程来说成本会降低很多,可以直接从自己线程的队列中获取任务而不需要去公共队列中争夺,有效的减少了线程间的资源竞争和切换。
有一种情况,当线程有多个如t1,t2,t3...,在某一段时间线程 t1 的任务特别繁重,分裂了数十个子任务,但是线程 t0 此时却无事可做,它自己的 deque 队列为空,这时为了提高效率,t0 就会想办法帮助 t1 执行任务,这就是“work-stealing”的含义。
双端队列 deque 中,线程 t1 获取任务的逻辑是后进先出,也就是LIFO(Last In Frist Out),而线程t0在“steal”偷线程 t1 的 deque 中的任务的逻辑是先进先出,也就是FIFO(Fast In Frist Out),如图所示,图中很好的描述了两个线程使用双端队列分别获取任务的情景。你可以看到,使用 “work-stealing” 算法和双端队列很好地平衡了各线程的负载。
本文由AnonyStar 发布,可转载但需声明原文出处。
欢迎关注微信公账号 :云栖简码 获取更多优质文章
更多文章关注笔者博客 :云栖简码 i-code.online