Java并发56:ForkJoin并发框架的原理、2种ForkJoinTask的用法以及ForkJoinPool的常用方法

本章主要对ForkJoin并发框架进行学习,主要内容分为三个部分:

  • ForkJoin并发框架的浅谈
  • ForkJoin并发编程的两个实例
  • ForkJoinPool线程池的常用方法说明

1.ForkJoin并发框架的浅谈

1.1.Fork和Join

ForkJoin并发框架:Fork=分解 + Join=合并

ForkJoin并发框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割(Fork)成若干个小任务,最终汇总(Join)每个小任务结果后得到大任务结果的框架。

例如:计算1+2+…1000000000,可以将其分割(Fork)为100000个小任务,每个任务计算10000个数据的相加,最终汇总(Join)这100000个小任务的计算结果进行合并,得到计算结果。

1.2.工作窃取算法

ForkJoin并发框架是采取工作窃取(Work-Stealing)算法实现的。

工作窃取算法某个线程从其他线程的工作队列中窃取任务来执行。可以形象的用下面的图表示:

 

 

下面来详细说明工作窃取算法(模拟):

  • 有一个较大的任务划分成了10个小任务。
  • 这10个小任务在一个大小为2的线程池中执行。
  • 线程池中的2个核心线程,每个线程的队列中有5个任务。
  • 线程1的任务都很简单,所以它很快就将5个任务执行完毕。
  • 线程2的任务都很复杂,当线程1执行完5个任务时,他才执行了3个任务。
  • 这时,线程1不会空闲,而且窃取线程2的等待队列中的任务(从末端开始窃取)来执行
  • 当线程2的队列中也没有了任务之后,线程1和线程2才空闲。

优缺点

  • 整体上,这种窃取算法,提高了线程利用率。
  • 为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列。
  • 存在两个线程共同竞争同一个任务的可能,例如双端队列中只有一个任务时。

1.3.编程思想

ForkJoin并发框架应用了两种十分重要的编程思想:

  • 分而治之
  • 递归

1.4.ForkJoin的主要类

ForkJoin并发框架的主要类包括:

  • ForkJoinPool:ForkJoin线程池,实现了ExecutorService接口和工作窃取算法,用于线程调度与管理
  • ForkJoinTask:ForkJoin任务,提供了fork()方法和join()方法。通常不直接使用,而是使用以下子类:
    • RecursiveAction:无返回值的任务,通常用于只fork不join的情形
    • RecursiveTask:有返回值的任务,通常用于fork+join的情形

1.5.ForkJoin的两类用法

根据ForkJoinTask的两种类型,可以将ForkJoin并发框架划分为两种用法:

  • only fork:递归划分子任务,分别执行,但是并不需要合并各自的执行结果。
  • fork+join:递归划分子任务,分别执行,然后递归合并计算结果。

only fork的示意图:

 

 fork+join的示意图:

 

 

2.实例编码

2.2.RecursiveAction实例编码

场景说明:

  • 真实场景:专网A内的数据库DB1上存储着100万条数据,需要通过数据交换服务发送到专网B的数据库DB2上。
  • 原来的古老做法:由于带宽和服务器性能等限制,每次发送的数据不能超过5000条。所以将这100万数据按照5000条一组进行分组,然后每组都通过一个线程进行发送。但是不知道什么原因,总之DB2中会经常出现重复数据。
  • 新的做法:根据ForkJoin框架编程思想,将这100万数据按照阈值THRESHOLD进行子任务划分,然后依次发送。

重点分析:

  • 这个场景只是将100万数据分组进行分发,并不需要再将分组合并,所以属于上述的第一种only fork用法。
  • 为了模拟对数据的接收,定义了一个ConcurrentLinkedQueue用于存储接收的数据。
  • 如果DB2最终的数据量与DB1的数据量一直,则表明数据发送成功。
  • 注意如何根据阈值THRESHOLD计算分组(fork())。
  • 注意递归分组的用法。

代码:

/**
 * <p>ForkJoin框架实例1-RecursiveAction-无返回值-数据交换</p>
 * <p>数据交换:专网A内的数据库DB1上有100万数据,需要通过数据交换服务发送到专网B的数据库DB2上。
 * 1.原来的做法:将这100万数据按照5000条一组进行分组,然后每组都通过一个线程进行发送。不知道什么原因,总之经常会出现重复发送的数据。
 * 2.新的做法:根据ForkJoin框架编程思想,将这100万数据按照阈值THRESHOLD进行子任务划分,然后依次发送。</p>
 *
 * @author hanchao 2018/4/15 19:26
 **/
public class RecursiveActionDemo {
    private static final Logger LOGGER = Logger.getLogger(RecursiveActionDemo.class);
    //模拟数据库DB2
    static ConcurrentLinkedQueue DB2 = new ConcurrentLinkedQueue();

    /**
     * <p>定义一个数据交换任务,继承自RecursiveAction,用于发送数据交换的JSON数据</p>
     *
     * @author hanchao 2018/4/15 19:28
     **/
    static class DataExchangeTask extends RecursiveAction {

        //阈值=5000
        private static final int THRESHOLD = 5000;
        //开始索引
        private int start;
        //结束索引
        private int end;
        //交换的数据
        List<String> list;

        public DataExchangeTask(int start, int end, List<String> list) {
            this.start = start;
            this.end = end;
            this.list = list;
        }

        @Override
        protected void compute() {
            //如果当前任务数量在阈值范围内,则发送数据
            if (end - start < THRESHOLD) {
                //发送Json数据
                try {
                    sendJsonDate(this.list);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                //如果当前任务数量超出阈值,则进行任务拆分
                int middle = (start + end) / 2;
                //左边的子任务
                DataExchangeTask left = new DataExchangeTask(start, middle, list);
                //右边的子任务
                DataExchangeTask right = new DataExchangeTask(middle, end, list);
                //并行执行两个“小任务”
                left.fork();
                right.fork();
            }
        }

        /**
         * <p>发送数据</p>
         *
         * @author hanchao 2018/4/15 20:04
         **/
        private void sendJsonDate(List<String> list) throws InterruptedException {
            //遍历
            for (int i = start; i < end; i++) {
                //每个元素都插入到DB2中 ==> 模拟数据发送到DB2
                DB2.add(list.get(i));
            }
            //假定每次发送耗时1ms
            Thread.sleep(1);
        }
    }

    /**
     * <p>模拟从数据库中查询数据并形成JSON个是的数据</p>
     *
     * @author hanchao 2018/4/15 20:21
     **/
    static void queryDataToJson(List list) {
        //随机获取100万~110万个数据
        int count = RandomUtils.nextInt(1000000, 1100000);
        for (int i = 0; i < count; i++) {
            list.add("{\"id\":\"" + UUID.randomUUID() + "\"}");
        }
    }

    /**
     * <p>RecursiveAction-无返回值:可以看成只有fork没有join</p>
     *
     * @author hanchao 2018/4/15 19:26
     **/
    public static void main(String[] args) throws InterruptedException {
        //从数据库中获取所有需要交换的数据
        List dataList = new ArrayList<String>();
        queryDataToJson(dataList);
        int count = dataList.size();
        LOGGER.info("1.从DB1中读取数据并存放到List中,共计读取了" + count + "条数据.");

        //DB2的数据量
        LOGGER.info("2.开始时,DB2中的数据量:" + DB2.size());

        LOGGER.info("3.通过ForkJoin框架进行子任务划分,并发送数据");
        //定义一个ForkJoin线程池
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        //定义一个可分解的任务
        DataExchangeTask dataExchangeTask = new DataExchangeTask(0, count, dataList);
        //向ForkJoin线程池提交任务
        forkJoinPool.submit(dataExchangeTask);
        //线程阻塞,等待所有任务完成
        forkJoinPool.awaitTermination(5, TimeUnit.SECONDS);
        //任务完成之后关闭线程池
        forkJoinPool.shutdown();

        //查询最终传输的数据量
        LOGGER.info("4.结束时,DB2中的数据量:" + DB2.size());
        //查询其中一条数据
        LOGGER.info("5.查询其中一条数据:" + DB2.peek());
    }
}

运行结果:

2018-04-17 23:38:05 INFO - 1.从DB1中读取数据并存放到List中,共计读取了1037606条数据.
2018-04-17 23:38:05 INFO - 2.开始时,DB2中的数据量:0
2018-04-17 23:38:05 INFO - 3.通过ForkJoin框架进行子任务划分,并发送数据
2018-04-17 23:38:10 INFO - 4.结束时,DB2中的数据量:1037606
2018-04-17 23:38:10 INFO - 5.查询其中一条数据:{"id":"85bc8085-4836-41e3-b7f4-d80f38d8f0fe"}

运行结果,表明发送数据成功。

2.2.RecursiveTask实例编码

场景说明:

  • 连续整数求和:N,N+1,N+2,N+3…N+MAX
  • 举例:1+2+3+4+…+1000000000=500000000500000000
  • 第一种方式:单线程计算
  • 第二种方式:ForkJoin并发计算

重点分析:

  • 需要将1000000000个数据分成若干组,分别求和,然后合并计算结果。所以属于上述的第二种fork+join用法。
  • 注意如何根据阈值THRESHOLD计算分组(fork())。
  • 注意递归fork()的用法。
  • 注意递归join()的用法。

代码:

/**
 * <p>ForkJoin框架-RecursiveTask-有返回值-超大集合分割计算</p>
 * <p>计算N,N+1,N+2....N+Max的和</p>
 * <p>第一种方式:单线程计算</p>
 * <p>第二种方式:ForkJoin并发计算</p>
 * @author hanchao 2018/4/15 21:31
 **/
public class RecursiveTaskDemo {
    private static final Logger LOGGER = Logger.getLogger(RecursiveTaskDemo.class);

    /**
     * <p>超大集合计算任务-泛型类</p>
     *
     * @author hanchao 2018/4/15 21:34
     **/
    static class LargeSetComputeTask extends RecursiveTask<Long> {

        //阈值
        private static final int THRESHOLD = 100000;
        private int start;//开始下标
        private int end;//结束下标

        public LargeSetComputeTask(int start, int end) {
            this.start = start;
            this.end = end;
        }

        @Override
        protected Long compute() {
            //如果当前任务的计算量在阈值范围内,则直接进行计算
            if (end - start < THRESHOLD) {
                return computeByUnit();
            } else {//如果当前任务的计算量超出阈值范围,则进行计算任务拆分
                //计算中间索引
                int middle = (start + end) / 2;
                //定义子任务-迭代思想
                LargeSetComputeTask left = new LargeSetComputeTask(start, middle);
                LargeSetComputeTask right = new LargeSetComputeTask(middle, end);
                //划分子任务-fork
                left.fork();
                right.fork();
                //合并计算结果-join
                return left.join() + right.join();
            }
        }

        /**
         * <p>最小计算单元进行计算</p>
         *
         * @author hanchao 2018/4/15 21:39
         **/
        private long computeByUnit() {
            long sum = 0L;
            for (int i = start; i < end; i++) {
                sum += i;
            }
            return sum;
        }
    }

    /**
     * <p>ForkJoin框架-RecursiveTask</p>
     * <p>1.有返回值:可用Future接口进行结果获取</p>
     * <p>2.RecursiveTask需要fork和join并用</p>
     *
     * @author hanchao 2018/4/15 21:44
     **/
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //计算(0+1+2+3+1000000000)*2的结果
        int count = 1000000001;

        //第一种方式:单线程计算
        long start1 = System.currentTimeMillis();
        LOGGER.info("1.第一种计算方式--单线程计算");
        long result = 0L;
        for (long i = 0; i < count; i++) {
            result += i;
        }
        LOGGER.info("1.计算结果:" + result + ",用时:" + (System.currentTimeMillis() - start1) + "ms.\n");

        //通过ForkJoin框架进行子任务计算
        long start2 = System.currentTimeMillis();
        LOGGER.info("2.第二种计算方式--ForkJoin框架计算");
        //定义ForkJoinPool线程池
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        //定义计算任务
        LargeSetComputeTask computeTask = new LargeSetComputeTask(0, count);
        //提交计算任务
        Future<Long> future = forkJoinPool.submit(computeTask);
        //执行完任务关闭线程池
        forkJoinPool.shutdown();
        //输出计算结果:
        LOGGER.info("2.计算结果:" + future.get() + ",用时:" + (System.currentTimeMillis() - start2) + "ms.");
    }
}

运行结果:

2018-04-17 23:52:45 INFO - 1.第一种计算方式--单线程计算
2018-04-17 23:52:45 INFO - 1.计算结果:500000000500000000,用时:338ms.

2018-04-17 23:52:45 INFO - 2.第二种计算方式--ForkJoin框架计算
2018-04-17 23:52:45 INFO - 2.计算结果:500000000500000000,用时:213ms.

运行结果说明两种方式计算结果都正确,fork+join效率高。

3.ForkJoinPool的常用方法

上面的两个实例对ForkJoin并发框架的编程方式进行了入门介绍。

为了更加全面的了解ForkJoin并发框架,下面对ForkJoinPool的常用方法进行简单的罗列:

/**
 * <p>ForkJoin-ForkJoinPool的方法学习</p>
 *
 * @author hanchao 2018/4/15 22:12
 **/
public class ForkJoinPoolBasicDemo {
    /**
     * <p>ForkJoin-ForkJoinPool的方法学习</p>
     *
     * @author hanchao 2018/4/15 22:14
     **/
    public static void main(String[] args) {
        //构造函数
        //无参:并行级别=Runtime.getRuntime.availableProcessors();
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        //指定并行级别
        ForkJoinPool forkJoinPool1 = new ForkJoinPool(4);

        //一:提交任务(返回计算情况)
        //ForkJoinTask<V> implements Future<V>, Serializable
        //提交Runnable任务
        Runnable runnable = null;
        forkJoinPool.submit(runnable);
        //提交Runnable + result任务
        Integer result = null;
        Future<Integer> future2 = forkJoinPool.submit(runnable, result);
        //提交Callable<V>任务
        Callable<Integer> callable = null;
        Future<Integer> future3 = forkJoinPool.submit(callable);
        //提交ForkJoinTask<V>任务
        ForkJoinTask<Integer> forkJoinTask = null;
        Future<Integer> future4 = forkJoinPool.submit(forkJoinTask);
        //提交RecursiveAction任务(RecursiveAction extends ForkJoinTask<Void>)
        RecursiveAction recursiveAction = null;
        forkJoinPool.submit(recursiveAction);
        //提交RecursiveTask<V>任务(RecursiveTask<V> extends ForkJoinTask<V>)
        RecursiveTask<Integer> recursiveTask = null;
        Future<Integer> future6 = forkJoinPool.submit(recursiveTask);

        //二:提交任务(不返回计算情况)
        //提交Runnable任务
        Runnable runnable1 = null;
        forkJoinPool.execute(runnable1);
        //提交ForkJoinTask<V>任务
        ForkJoinTask<Integer> forkJoinTask1 = null;
        forkJoinPool.execute(forkJoinTask);
        //提交RecursiveAction任务(RecursiveAction extends ForkJoinTask<Void>)
        RecursiveAction recursiveAction1 = null;
        forkJoinPool.execute(recursiveAction);
        //提交RecursiveTask<V>任务(RecursiveTask<V> extends ForkJoinTask<V>)
        RecursiveTask<Integer> recursiveTask1 = null;
        forkJoinPool.execute(recursiveTask);

        //三:提交任务(返回计算结果)
        //提交ForkJoinTask<V>任务
        ForkJoinTask<Integer> forkJoinTask2 = null;
        Integer result1 = forkJoinPool.invoke(forkJoinTask);
        //提交RecursiveAction任务(RecursiveAction extends ForkJoinTask<Void>)
        RecursiveAction recursiveAction2 = null;
        forkJoinPool.invoke(recursiveAction);
        //提交RecursiveTask<V>任务(RecursiveTask<V> extends ForkJoinTask<V>)
        RecursiveTask<Integer> recursiveTask2 = null;
        Integer result3 = forkJoinPool.invoke(recursiveTask);

        //四:提交任务集
        //获取最先计算完成的-阻塞
        List<Callable<Integer>> callableList = new ArrayList<Callable<Integer>>();
        try {
            Integer result4 = forkJoinPool.invokeAny(callableList);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        //获取最先计算完成的-阻塞-可超时
        try {
            Integer result5 = forkJoinPool.invokeAny(callableList, 1, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
        //所有任务计算完成之后,返回结果-阻塞
        List<Future<Integer>> futureList = forkJoinPool.invokeAll(callableList);
        //所有任务计算完成之后,返回结果-阻塞-可超时
        try {
            List<Future<Integer>> futureList1 = forkJoinPool.invokeAll(callableList, 1, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //是否正在终止
        forkJoinPool.isTerminating();
        //是否终止
        forkJoinPool.isTerminated();
        try {
            //等待终止
            forkJoinPool.awaitTermination(1, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //是否休眠
        forkJoinPool.isQuiescent();
        //等待休眠
        forkJoinPool.awaitQuiescence(1, TimeUnit.SECONDS);

        //存在等待执行的子任务
        forkJoinPool.hasQueuedSubmissions();

        //是否是FIFO模式
        boolean asyncMode = forkJoinPool.getAsyncMode();
        //获取当前活跃线程数
        int activeThreadCount = forkJoinPool.getActiveThreadCount();
        //获取线程池并行级别
        int parallelism = forkJoinPool.getParallelism();
        //获取工作线程数量
        int poolSize = forkJoinPool.getPoolSize();
        //获取等待执行的子任务数量
        int queuedSubmissionCount = forkJoinPool.getQueuedSubmissionCount();
        //获取等待执行的任务数量
        long queuedTaskCount = forkJoinPool.getQueuedTaskCount();
        //获取非阻塞的活动线程数量
        int runningThreadCount = forkJoinPool.getRunningThreadCount();
        //获取窃取线程数量
        long stealCount = forkJoinPool.getStealCount();
        //获取工作线程工厂
        ForkJoinPool.ForkJoinWorkerThreadFactory threadFactory = forkJoinPool.getFactory();
        //获取未捕获异常处理器
        Thread.UncaughtExceptionHandler handler = forkJoinPool.getUncaughtExceptionHandler();

        //关闭线程池
        forkJoinPool.isShutdown();
        forkJoinPool.shutdown();
        forkJoinPool.shutdownNow();
    }
}

 

posted @ 2021-10-23 15:40  姚春辉  阅读(302)  评论(1编辑  收藏  举报