Java并发工具类之ForkJoin

in 网站建设
关注公众号【好便宜】( ID:haopianyi222 ),领红包啦~
阿里云,国内最大的云服务商,注册就送数千元优惠券:https://t.cn/AiQe5A0g
腾讯云,良心云,价格优惠: https://t.cn/AieHwwKl
搬瓦工,CN2 GIA 优质线路,搭梯子、海外建站推荐: https://t.cn/AieHwfX9

1、是什么

1.1 什么是ForkJoin

java.util.concurrent.ForkJoinPool由Java大师Doug Lea主持编写,它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出。

ForkJoin是由JDK1.7后提供多线并发处理框架。ForkJoin的框架的基本思想是分而治之。什么是分而治之?分而治之就是将一个复杂的计算,按照设定的阈值进行分解成多个计算,然后将各个计算结果进行汇总。相应的ForkJoin将复杂的计算当做一个任务。而分解的多个计算则是当做一个子任务。

下面有些图是借鉴了网上的资料:
参考:

https://blog.csdn.net/tyrroo/article/details/81390202

https://ifeve.com/forkjoin/#more-9081

2、ForkJoin的概述

2.1 ForkJoin的设计思路

设计思路,借用网上的一个图:

image.png

forkjoin大体的执行过程就如上图所示,先把一个大任务分解(fork)成许多个独立的小任务,然后起多线程并行去处理这些小任务。处理完得到结果后再进行合并(join)就得到我们的最终结果。同时forkjoin内部还运用了一种叫work-stealing(工作窃取)的算法,这种算法的设计思路在于把分解出来的小任务放在多个双端队列中,而线程在队列的头和尾部都可获取任务。当有线程把当前负责队列的任务处理完之后,它还可以从那些还没有处理完的队列的尾部窃取任务来处理,这连线程的空余时间也充分利用了!

2.1 ForkJoin的主要类

在了解怎么用forkjoin之前,我们先了解下forkjoin是怎么实现这些功能的,框架中包括哪些类,分别是做什么的。这里介绍JDK里面与forkjoin相关的主要几个类:

1)ForkJoinPool:是forkjoin框架里面的管理者,最原始的任务都要交给它才能处理。它负责控制整个forkjoin有多少个workerThread,workerThread的创建,激活都是由它来掌控。它还负责workQueue队列的创建和分配,每当创建一个workerThread,它负责分配相应的workQueue。然后它把接到的活都交给workerThread去处理,它可以说是整个forkjoin的容器。

2)ForkJoinWorkerThread:forkjoin里面真正干活的"工人",是一个线程。里面有一个ForkJoinPool.WorkQueue的队列存放着它要干的活,接活之前它要向ForkJoinPool注册(registerWorker),拿到相应的workQueue。然后就从workQueue里面拿任务出来处理。它是依附于ForkJoinPool而存活,如果ForkJoinPool的销毁了,它也会跟着结束。

3)ForkJoinPool.WorkQueue: 双端队列就是它,它负责存储接收的任务。

4)ForkJoinTask:是RecursiveAction与RecursiveTask的父类, ForkJoinTask中使用了模板模式进行设计,将ForkJoinTask的执行相关的代码进行隐藏,我们一般用它的两个子类RecursiveTask、RecursiveAction。这两个区别在于RecursiveTask任务是有返回值,RecursiveAction没有返回值。任务的处理逻辑和任务的切分都集中在compute()方法里面。

下面我们看一个简单的实例,这个是网上的例子上修改来的。

3、ForkJoin的使用

3.1 基本使用

使用fork/join框架的第一步是编写执行一部分工作的代码。你的代码结构看起来应该与下面所示的伪代码类似:

阀值来判断任务是应该直接完成还是应该被拆分。 if (当前这个任务工作量足够小) 直接完成这个任务 else 将这个任务或这部分工作分解成两个部分 分别触发(invoke)这两个子任务的执行,并等待结果

3.2 简单示例

package com.topinfo.test.forkjoin;

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;

/**
 *@Description: 这是一个简单的Join/Fork计算过程,将数组内容相加
 *@Author:杨攀
 *@Since:2019年12月19日下午4:42:41
 */
public class TestForkJoinPool {
    // 阀值
    private static final Integer MAX = 4;

    static class MyForkJoinTask extends RecursiveTask<Integer> {
        
        // 子任务开始计算的值
        private Integer startValue;

        // 子任务结束计算的值
        private Integer endValue;
        
        // 需要计算的内容
        private int[] data;

        public MyForkJoinTask(Integer startValue, Integer endValue, int[] data) {
            this.startValue = startValue;
            this.endValue = endValue;
            this.data = data;
        }

        @Override
        protected Integer compute() {
            // 如果条件成立,说明这个任务所需要计算的数值分为足够小了
            // 可以正式进行累加计算了
            boolean isCompute = (endValue - startValue) < MAX;
            
            if (isCompute) {
                
                Integer totalValue = 0;
                
                for (int i = this.startValue; i <= this.endValue; i++) {
                    totalValue += data[i];
                }
                
                System.out.println("线程:"+Thread.currentThread().getId() +"-开始计算的部分:startValue = " + startValue + "-endValue = " + endValue + "- 间隔:"+(endValue - startValue));
                
                return totalValue;
            }
            else {
                // 否则再进行任务拆分,拆分成两个任务
                int middle = (startValue + endValue) / 2;
                
                MyForkJoinTask subTask1 = new MyForkJoinTask(startValue, middle, data);
                MyForkJoinTask subTask2 = new MyForkJoinTask(middle + 1, endValue, data);
                
                // 任务的批量提交, 执行拆分
                invokeAll(subTask1, subTask2 );
                
                // 等待子任务执行完返回,合并其结果
                int sum = subTask1.join() + subTask2.join();
                
                return sum;
            }
        }
    }

    public static void main(String[] args) {
        
        int[]  data = {3, 5, 6, 8 ,1, 0, 9, 7, 5, 8, 10, 23, 7, 8, 10};
        
        int sum = 0;
        for (int i = 0; i < data.length; i++) {
            sum += data[i];
        }
        System.out.println("data:" + sum);
        
        // -----------------------------------------------------------
        
        // 这是Fork/Join框架的线程池
        ForkJoinPool pool = new ForkJoinPool();
        // 单例获取方式
        // ForkJoinPool pool = ForkJoinPool.commonPool();
        
        int start = 0;
        int end = data.length - 1;
        
        ForkJoinTask<Integer> taskFuture = pool.submit(new MyForkJoinTask(start, end, data));
        try {
            
            Integer result = taskFuture.get();
            System.out.println("result = " + result);
        } catch (Exception e) {
            e.printStackTrace(System.out);
        }
    }
}

结果:
image.png

仔细分析代码,子任务在compute()方法内,根据阈值进行比对,如果小于阈值,表示可以已经足够小了,可以执行计算了,否则执行拆分计算,在执行递归调用,当足够小的时候,其内部使用多线程并行完成这些小任务的计算后再进行结果向上的合并动作,最终形成顶层结果。

4、ForkJoin的详解

4.1 ForkJoinPool

ForkJoinPool的构造函数,其中默认的那个构造函数如下所示:

public ForkJoinPool(int parallelism, ForkJoinWorkerThreadFactory factory, UncaughtExceptionHandler handler, boolean asyncMode) {

parallelism: 可并行级别,ForkJoin框架将依据这个并行级别的设定,决定框架内并行执行的线程数量。

factory:ForkJoin框架创建一个新的线程的工厂类,在Fork/Join框架中有一个默认的ForkJoinWorkerThreadFactory接口实现:DefaultForkJoinWorkerThreadFactory。

handler:异常捕获处理器。当执行的任务中出现异常,并从任务中被抛出时,就会被handler捕获。

asyncMode:异步模式,ForkJoin框架中为每一个独立工作的线程准备了对应的待执行任务队列,这个任务队列是使用数组进行组合的双向队列。asyncMode确定队列中待执行的任务的工作模式,当asyncMode设置为ture的时候,队列采用先进先出方式工作;反之则是采用后进先出的方式工作,该值默认为false。

4.1 ForkJoin的常用方法解释

ForkJoinTask的常用方法有:

invokeAll方法:在fork/join模式中,我们在子任务中常常使用fork方法来让子任务采取异步方法执行,但是这不是高效的实现方法,尤其是对于forkjoinPool在线程有限的情况下,子任务直接使用fork方法执行时间比使用invokeAll执行时间要长。因为pool里面线程数量是固定的,那么调用子任务的fork方法相当于A先分工给B,然后A当监工不干活,B去完成A交代的任务。所以上面的模式相当于浪费了一个线程。那么如果使用invokeAll相当于A分工给B后,A和B都去完成工作。这样缩短了执行的时间。

join方法:用于让当前线程阻塞,直到对应的子任务完成运行并返回执行结果。或者,如果这个子任务存在于当前线程的任务等待队列(work queue)中,则取出这个子任务进行“递归”执行。其目的是尽快得到当前子任务的运行结果,然后继续执行。

ForkJoinPool执行任务的方法有:

execute(ForkJoinTask) :异步执行tasks,无返回值。

invoke(ForkJoinTask) :有Join, tasks会被同步到主进程。

submit(ForkJoinTask) :异步执行,且带Task返回值,可通过task.get 实现同步到主线程。

5、使用Fork/Join解决实际问题

5.1 使用归并算法解决排序问题

排序问题是我们工作中的常见问题。目前也有很多现成算法是为了解决这个问题而被发明的,例如多种插值排序算法、多种交换排序算法。而并归排序算法是目前所有排序算法中,平均时间复杂度较好(O(nlgn)),算法稳定性较好的一种排序算法。它的核心算法思路将大的问题分解成多个小问题,并将结果进行合并。

image.png

整个算法的拆分阶段,是将未排序的数字集合,从一个较大集合递归拆分成若干较小的集合,这些较小的集合要么包含最多两个元素,要么就认为不够小需要继续进行拆分。

那么对于一个集合中元素的排序问题就变成了两个问题:1、较小集合中最多两个元素的大小排序;2、如何将两个有序集合合并成一个新的有序集合。第一个问题很好解决,那么第二个问题是否会很复杂呢?实际上第二个问题也很简单,只需要将两个集合同时进行一次遍历即可完成——比较当前集合中最小的元素,将最小元素放入新的集合,它的时间复杂度为O(n):

image.png

关注公众号【好便宜】( ID:haopianyi222 ),领红包啦~
阿里云,国内最大的云服务商,注册就送数千元优惠券:https://t.cn/AiQe5A0g
腾讯云,良心云,价格优惠: https://t.cn/AieHwwKl
搬瓦工,CN2 GIA 优质线路,搭梯子、海外建站推荐: https://t.cn/AieHwfX9
扫一扫关注公众号添加购物返利助手,领红包
Comments are closed.

推荐使用阿里云服务器

超多优惠券

服务器最低一折,一年不到100!

朕已阅去看看