Fork/Join分解合并框架

571 阅读2分钟

什么是fork/join

Fork/Join框架是JDK1.7提供的一个用于执行并发任务的框架,开发者可以在不去了解Thread、Runnable等相关知识的情况下,只要遵循fork/join开发模式,就可以写出很好的多线程任务。

同时按照分而治之的思想,可以把大任务分成若干个任务,最终汇总小任务的结果后得到大任务结果的框架。

对于Fork/Join框架的理解可以认为其由两部分组成,Fork就是把一个大任务切割为若干个小任务并发执行,Join就是合并这些小任务的结果,最后得到这个大任务的结果。

image.png

工作窃取算法

即当前线程的Task已经被全部执行完毕,则自动获取到其他线程的Task池中取出Task继续执行。ForkJoinPool中维护者多个线程(一般为CPU核数)在不断执行task,每个线程除了执行自己任务列表内的task之外,还会根据自己工作线程的闲置情况去获取其它繁忙的工作线程的Task,如此一来就能减少线程阻塞或是闲置的时间,提高CPU的利用率。

image.png

Fork/Join的使用

基本概念

要使用Fork/Join的话,首先需要一个Pool。通过它可以来执行任务。每一个任务叫做ForkJoinTask,其内部提高了fork和join的操作机制。通常开发者不需要直接继承ForkJoinTask,而是继承它的子类,分别为:

  • RecursiveAction:返回没有结果的任务
  • RecursiveTask:返回有结果的任务

image.png

  1. 新建ForkJoinPool
  2. 新建ForkJoinTask(RecursiveAction或RecursiveTask)
  3. 在任务中的compute方法中,会根据自定义条件进行任务拆分,如果条件满足则执行任务,如果条件不满足则继续拆分任务。当所有任务都执行完毕,进行最终结果的汇总。
  4. 最终通过join或get获取结果

同步有结果返回

需求:统计整型数组中所有元素的和

//生成随机数组
public class GenArray {

    //数组长度
    public static final int ARRAY_LENGTH=400000;

    public static int[] genArray(){

        Random random = new Random();
        int[] result = new int[ARRAY_LENGTH];
        for (int i = 0; i < ARRAY_LENGTH; i++) {
            //随机数填充数组
            result[i]= random.nextInt(ARRAY_LENGTH*3);
        }
        return result;
    }
}
//普通循环累加
public class SumNormal {

    public static void main(String[] args) {

        int count = 0;
        int[] src = GenArray.genArray();

        long start = System.currentTimeMillis();

        for (int i = 0; i < src.length; i++) {
            count+=src[i];
        }

        System.out.println("spend time: "+(System.currentTimeMillis()-start));
    }
}
//forkJoin累加
public class SumForkJoin{

    //自定义任务
    private static class SumTask extends RecursiveTask<Integer> {

        private final static int THRESHOLD=GenArray.ARRAY_LENGTH/10;
        private int[] src;
        private int fromIndex;
        private int endIndex;

        public SumTask(int[] src, int fromIndex, int endIndex) {
            this.src = src;
            this.fromIndex = fromIndex;
            this.endIndex = endIndex;
        }

        @Override
        protected Integer compute() {

            //判断是否符合任务大小
            if (endIndex-fromIndex<THRESHOLD){
                //符合条件
                int count = 0;
                for (int i = fromIndex; i <= endIndex; i++) {
                    count+=src[i];
                }
                return count;
            }else {
                //继续拆分任务
                //基于二分查找对任务进行拆分
                int mid = (fromIndex+endIndex)/2;
                SumTask left = new SumTask(src,fromIndex,mid);
                SumTask right = new SumTask(src,mid+1,endIndex);
                invokeAll(left,right);
                return left.join()+right.join();
            }
        }
    }

    public static void main(String[] args) {

        ForkJoinPool pool = new ForkJoinPool();

        int[] src = GenArray.genArray();
        SumTask sumTask = new SumTask(src,0,src.length-1);

        long start = System.currentTimeMillis();
        pool.invoke(sumTask);
        System.out.println("spend time: "+(System.currentTimeMillis()-start));
    }
}

根据执行结果来看,如果数据量比较小的情况下,使用普通循环的效率更高,因为其内部是以总线的形式进行相加。而fork/join的话,要利用当前可用的CPU核数结合线程的上下文切换,所以存在一定的性能消耗,但是如果数据量大的话,可以看到forkjoin的效率明显高于普通for循环

异步无结果值返回

需求:遍历目录(包含子目录)寻找txt类型文件

public class FindFile extends RecursiveAction {

    private File path;

    public FindFile(File path) {
        this.path = path;
    }

    @Override
    protected void compute() {
        List<FindFile> takes = new ArrayList<>();

        //获取指定路径下的所有文件
        File[] files = path.listFiles();

        if (files != null){
            for (File file : files) {
                //是否为文件夹
                if (file.isDirectory()){
                    //递归调用
                    takes.add(new FindFile(file));
                }else {
                    //不是文件夹。执行检查
                    if (file.getAbsolutePath().endsWith("txt")){
                        System.out.println(file.getAbsolutePath());
                    }
                }
            }

            //调度所有子任务执行
            if (!takes.isEmpty()){
                for (FindFile task : invokeAll(takes)){
                    //阻塞当前线程并等待获取结果
                    task.join();
                }
            }
        }
    }

    public static void main(String[] args) {

        ForkJoinPool pool = new ForkJoinPool();

        FindFile task = new FindFile(new File("F://"));

        pool.submit(task);

        //主线程join,等待子任务执行完毕。
        task.join();

        System.out.println("task end");
    }
}