并发编程学习笔记6—— Fork / Join 的简单使用


Java 并发包里提供了一种叫做 Fork/Join 的并行计算框架,用来支持分治。

1. 分治任务模型

分治任务模型可分为两个阶段:一个阶段是任务分解,也就是将任务迭代地分解为子任务,直至子任务可以直接计算出结果;另一个阶段是结果合并,即逐层合并子任务的执行结果,直至获得最终结果。下图是一个简化的分治任务模型图。
在这里插入图片描述

任务和分解后的子任务具有相似性,这种相似性往往体现在任务和子任务的算法是相同的,但是计算的数据规模是不同的。具备这种相似性的问题,我们往往都采用递归算法。

2. Fork/Join 的使用

Fork/Join 计算框架主要包含两部分,一部分是分治任务的线程池 ForkJoinPool,另一部分是分治任务 ForkJoinTask。这两部分的关系类似于 ThreadPoolExecutor 和 Runnable 的关系。

ForkJoinTask 是一个抽象类,最核心的是 fork() 方法和 join() 方法,其中 fork() 方法会异步地执行一个子任务,而 join() 方法则会阻塞当前线程来等待子任务的执行结果。

ForkJoinTask 有两个子类——RecursiveActionRecursiveTask,它们都是用递归的方式来处理分治任务的。区别是 RecursiveAction 定义的 compute() 没有返回值,而 RecursiveTask 定义的 compute() 方法是有返回值的。这两个子类也是抽象类,在使用的时候,需要你定义子类去扩展。

用 Fork / Join 实现斐波那契数列计算 对比 传统递归 :

public class TestForkJoin {
    public static void main(String[] args) {
//        testNew();
        testOld();
    }

    static void testNew() {
        long start = System.currentTimeMillis();
        ForkJoinPool pool = new ForkJoinPool(3);
        Fib f = new Fib(30);
        Integer result = pool.invoke(f);
        System.out.println(result);
        long end = System.currentTimeMillis();
        System.out.println("耗时: " + (end - start) + " ms");
    }

    static void testOld() {
        long start = System.currentTimeMillis();
        Fib f = new Fib(30);
        Integer result = f.oldCompute();
        System.out.println(result);
        long end = System.currentTimeMillis();
        System.out.println("耗时: " + (end - start) + " ms");
    }


    static class Fib extends RecursiveTask<Integer> {
        int n;

        public Fib(int n) {
            this.n = n;
        }

        @Override
        protected Integer compute() {
            if (n <= 1) {
                return n;
            }
            Fib f1 = new Fib(n - 1);
            f1.fork();
            Fib f2 = new Fib(n - 2);
            return f2.compute() + f1.join();
            //不能反过来写,否则会阻塞在join
//            return f1.join()+f2.compute();
        }

        public Integer oldCompute() {
            if (n <= 1) {
                return n;
            }
            Fib f1 = new Fib(n - 1);
            Fib f2 = new Fib(n - 2);
            return f1.compute() + f2.compute();
        }
    }
}

3. ForkJoinPool 工作原理

ThreadPoolExecutor 本质上是一个生产者 - 消费者模式的实现,内部有一个任务队列,这个任务队列是生产者和消费者通信的媒介;ThreadPoolExecutor 可以有多个工作线程,但是这些工作线程都共享一个任务队列。

ForkJoinPool 本质上也是一个生产者 - 消费者的实现,而 ForkJoinPool 内部有多个任务队列,当我们通过 ForkJoinPool 的invoke() 或者 submit()方法提交任务时,ForkJoinPool 根据一定的路由规则把任务提交到一个任务队列中,如果任务在执行过程中会创建出子任务,那么子任务会提交到工作线程对应的任务队列中。

ForkJoinPool 支持一种叫做“任务窃取”的机制,如果工作线程空闲了,那它可以“窃取”其他工作任务队列里的任务。

ForkJoinPool 中的任务队列采用的是双端队列,工作线程正常获取任务和“窃取任务”分别是从任务队列不同的端消费,这样能避免很多不必要的数据竞争。
在这里插入图片描述
思考题
对于一个 CPU 密集型计算程序,在单核 CPU 上,使用 Fork/Join 并行计算框架是否能够提高性能呢?

单核CPU同一时间只能处理一个线程,所以理论上,纯cpu密集型计算任务单线程就够了。多线程的话,线程上下文切换带来的线程现场保存和恢复也会带来额外开销。


参考资料:王宝令----Java并发编程实战

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值