• Java并发


    1. 什么是 Fork/Join 框架

    Fork/Join 框架是 Java7 提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架 

    它的主要思想是:分而治之

    我们再通过 Fork 和 Join 这两个单词来理解下 Fork/Join 框架,Fork 就是把一个大任务切分为若干子任务并行的执行,Join 就是合并这些子任务的执行结果,最后得到这个大任务的结果。

    比如:计算 1+2+... +10000,可以分割成 10 个子任务,每个子任务分别对 1000 个数进行求和,最终汇总这 10 个子任务的结果。

    Fork/Join 的运行流程图如下

    Fork/Join任务的原理:判断一个任务是否足够小,如果是,直接计算,否则,就分拆成几个小任务分别计算。这个过程可以反复“裂变”成一系列小任务。

    2. 工作窃取算法

    工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。工作窃取的运行流程图如下:

    为什么 ForkJoin 会存在工作窃取呢?因为我们将任务进行分解成多个子任务的时候。每个子任务的处理时间都不一样。

    例如分别有子任务 A B 。如果子任务 A 的 1ms 的时候已经执行,子任务 B 还在执行。那么如果我们子任务 A 的线程等待子任务 B 完毕后在进行汇总,那么子任务 A 线程就会在浪费执行时间,最终的执行时间就以最耗时的子任务为准。而如果我们的子任务A执行完毕后,处理子任务 B 的任务,并且执行完毕后将任务归还给子任务 B。这样就可以提高执行效率。而这种就是工作窃取。

    工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。

    3. Fork/Join 框架的介绍

    我们已经很清楚 Fork/Join 框架的需求了,那么我们可以思考一下,如果让我们来设计一个 Fork/Join 框架,该如何设计?这个思考有助于你理解 Fork/Join 框架的设计。

    1)Fork/Join 框架的设计分为两步

    • 第一步分割任务。首先我们需要有一个 fork 类来把大任务分割成子任务,有可能子任务还是很大,所以还需要不停的分割,直到分割出的子任务足够小。
    • 第二步执行任务并合并结果。分割的子任务分别放在双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据。

    2)Fork/Join 使用两个类来完成以上两件事情

    1. ForkJoinTask:我们要使用 ForkJoin 框架,必须首先创建一个 ForkJoin 任务。它提供在任务中执行 fork() 和 join() 操作的机制,通常情况下我们不需要直接继承 ForkJoinTask 类,而只需要继承它的子类,Fork/Join 框架提供了以下两个子类:
      • RecursiveAction:用于没有返回结果的任务。
      • RecursiveTask :用于有返回结果的任务。
    2. ForkJoinPool :ForkJoinTask 需要通过 ForkJoinPool 来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。

    4. 使用 Fork/Join 框架

    使用 Fork/Join 框架计算:1+2+3+……+100000000。

    使用 Fork/Join 框架首先要考虑到的是如何分割任务,如果我们希望每个子任务最多执行 10000 个数的相加,那么我们设置分割的阈值是 10000,由于是 100000000 个数字相加,所以会不停的分割,第一次先分割成两部分,即 1~50000000 和 50000001~100000000,第二次继续将 1~50000000 分割成 1~25000000 和 25000001~50000000 ,将50000001~100000000 分割成 50000001~75000000 和 75000001~100000000 ……,一直分割,直到 开始和 结束的的差小于等于 10000 。

    4-1. 创建 ForkJoin 任务

    使用 ForkJoin 框架,必须首先创建一个 ForkJoin 任务。

    public class ForkJoinDemo extends RecursiveTask<Long> {
    
        private long start; // 开始值
        private long end;   // 结束值
    
        private long temp = 10000L;  // 阈值
        public ForkJoinDemo(long start, long end) {
            this.start = start;
            this.end = end;
        }
    
        @Override
        protected Long compute() {
    
            // 条件成立:任务量没有超过临界值时计算;
            if ((end - start) < temp) {
    
                Long sum = 0L;
                for (long i = start; i <= end; i++) {
                    sum += i;
                }
                return sum;
    
            } else { // 条件不成立:拆分任务
    
                long middle = (end + start) / 2; // 中间值
    
    // 进行递归 ForkJoinDemo fork_1 = new ForkJoinDemo(start, middle); // fork_1.fork(); // fork直接这样使用会导致有一个线程变成boss线程。执行时间会变长。 ForkJoinDemo fork_2 = new ForkJoinDemo(middle + 1, end); // fork_2.fork(); // fork直接这样使用会导致有一个线程变成boss线程。执行时间会变长。 // 执行子任务,应该使用invokeAll(left,right);这样代码效率成倍提升 invokeAll(fork_1,fork_2);
    // 返回结果
    return fork_1.join() + fork_2.join(); } } }

    4-2. 使用 ForkJoinPool 执行任务

    task 要通过 ForkJoinPool 来执行,分割的子任务也会添加到当前工作线程的双端队列中,进入队列的头部。

    当一个工作线程中没有任务时,会从其他工作线程的队列尾部获取一个任务(工作窃取)。

    public class Test {
    
        public static void main(String[] args) throws ExecutionException, InterruptedException {
    
            long start = System.currentTimeMillis();
    
            // 这是Fork/Join框架的线程池
            ForkJoinPool pool = new ForkJoinPool();
            ForkJoinTask<Long> submit = pool.submit(new ForkJoinDemo(0L, 1_0000_0000L));
            // 提交任务
            Long sum = submit.get();
    
            long end = System.currentTimeMillis();
            System.out.println("sum = " + sum + " ,耗时:" + (end - start) + " 毫秒");
        }
    }

    执行结果:

    sum = 5000000050000000 ,耗时:837 毫秒

    三种提交任务到 ForkJoinPool 的方法

    1. execute():异步执行,没有任何返回 。
    2. invoke():  同步执行,调用之后需要等待任务完成,才能执行后面的代码 。
    3. submit():  异步执行,当调用get方法的时候会阻塞,完成时返回一个future对象用于检查状态以及运行结果。
    ForkJoinPool commonPool = ForkJoinPool.commonPool();

    为公共池提供一个引用,使用预定义的公共池减少了资源消耗,因为这阻碍了每个任务创建一个单独的线程池。

    5. Fork/Join 框架的异常处理

    ForkJoinTask 在执行的时候可能会抛出异常,但是我们没办法在主线程里直接捕获异常,所以 ForkJoinTask 提供了 isCompletedAbnormally() 方法来检查任务是否已经抛出异常或已经被取消了,并且可以通过 ForkJoinTask 的 getException 方法获取异常 。使用如下代码:

    if(task.isCompletedAbnormally()){
        System.out.println(task.getException());
    }

    getException 方法返回 Throwable 对象,如果任务被取消了则返回 CancellationException。如果任务没有完成或者没有抛出异常则返回 null。

    6. 注意点

    使用 ForkJoin 将相同的计算任务通过多线程的进行执行。从而能提高数据的计算速度。

    在 google 的中的大数据处理框架 mapreduce 就通过类似 ForkJoin 的思想。通过多线程提高大数据的处理。但是我们需要注意:

    • 使用这种多线程带来的数据共享问题,在处理结果的合并的时候如果涉及到数据共享的问题,我们尽可能使用 JDK 为我们提供的并发容器。
    • 在使用 JVM 的时候我们要考虑 OOM 的问题,如果我们的任务处理时间非常耗时,并且处理的数据非常大的时候。会造成 OOM。
    • ForkJoin 也是通过多线程的方式进行处理任务。那么我们不得不考虑是否应该使用 ForkJoin 。因为当数据量不是特别大的时候,我们没有必要使用 ForkJoin 。因为多线程会涉及到上下文的切换。所以数据量不大的时候使用串行比使用多线程快。
  • 相关阅读:
    打造属于自己的谷歌Maps版博客公告【演示+源码】
    CentOS 更改默认启动顺序
    VC的控制台程序,如何获取输入参数和环境变量
    如何同时提供ANSI和UNICODE函数
    VC工程需要注意字符集问题
    升级firefox到最新版
    VC工程需要注意字符集问题
    如何同时提供ANSI和UNICODE函数
    使C++ Builder编译程度独立运行
    安装谷歌浏览器
  • 原文地址:https://www.cnblogs.com/Dm920/p/13368664.html
Copyright © 2020-2023  润新知