【java并发核心八】Fork-Join分治编程

jdk1.7中提供了Fork/Join并行执行任务框架,主要作用就是把大任务分割成若干个小任务,再对每个小任务得到的结果进行汇总。

正常情况下,一些小任务我们可以使用单线程递归来实现,但是如果要想充分利用CPU资源,就需要把一个任务分成若干个小任务,并行执行了,这就是分治编程。

在JDK中,并行执行框架Fork-Join使用了“工作窃取(work-stealing)”算法。

JDK1.7中实现分治编程思路:

  使用 ForkJoinPool 类提供了一个任务池。

  具体执行任务需要靠 ForkJoinTask 类,而 ForkJoinTask 是抽象类,故使用该类的3个子类 CountedCompleter、RecursiveAction、RecursiveTask 来实现具体的功能。

  其中,RecursiveAction 执行的任务具有无返回值,且仅执行一次;RecursiveTask 执行任务可以通过方法 join() 或者 get() 取得方法返回值。

  补充, join() 和 get() 的区别:两个方法都可以获得计算后的结果值,区别是在子任务报异常时,get() 的异常可以在main主线程中进行捕获;而 join() 的异常会直接抛出。

看例子(求和:1+2+3+...+9999+10000):

package com.cd.thread;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;

public class ForkJoinTest {
    public static void main(String[] args) {
        final SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        System.out.println("start-time:" + sf.format(new Date()));
        addTest(); // for循环
        System.out.println("end-time:" + sf.format(new Date()));
        System.out.println("start-time:" + sf.format(new Date()));
        forkJoinTest();// 分治求和
        System.out.println("end-time:" + sf.format(new Date()));
    }

    private static void addTest() {
        int beginNum = 1, endNum = 10000, val = 0;
        for (int i = beginNum; i <= endNum; i++) {
            val += i;
        }
        System.out.println("for循环结果:" + val);
    }

    public static void forkJoinTest() {
        MyRecursiveTask task = new MyRecursiveTask(1, 10000);
        ForkJoinPool pool = new ForkJoinPool();
        ForkJoinTask<Integer> fjTask = pool.submit(task);
        try {
            System.out.println("分治求和结果:" + fjTask.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static class MyRecursiveTask extends RecursiveTask<Integer> {

        private Integer beginNum;

        private Integer endNum;

        private MyRecursiveTask(Integer beginNum, Integer endNum) {
            this.beginNum = beginNum;
            this.endNum = endNum;
        }

        @Override
        protected Integer compute() {
            if ((endNum - beginNum) > 500) {
                int middleNum = (endNum + beginNum) / 2;
                MyRecursiveTask task1 = new MyRecursiveTask(beginNum, middleNum);
                MyRecursiveTask task2 = new MyRecursiveTask(middleNum + 1, endNum);
                this.invokeAll(task1, task2);
                //                return task1.join() + task2.join();
                Integer num1 = 0, num2 = 0;
                try {
                    num1 = task1.get();
                    num2 = task2.get();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return num1 + num2;
            } else {
                Integer val = 0;
                for (int i = beginNum; i <= endNum; i++) {
                    val += i;
                }
                return val;
            }
        }

    }

}

从结果看,分治编程不一定会比单线程快,所以在用分治编程的时候,需要一定的测试才行。

而分治编程也有运用的领域,比如遍历一个目录及其子目录,处理一个树形结构算法问题。

在写代码的时候,会发现分治的代码看起来像递归,但是其实它们是并行执行的。

关于 ForkJoinPool 的 api,建议用到的时候,去看文档吧,看文档也是一种能力,也是一种技巧。

原文地址:https://www.cnblogs.com/klbc/p/9797969.html