java fork/join概述

前言

从JDK1.7开始,Java提供ForkJoin框架用于并行执行任务,它的思想就是基于“分治”,它将一个大任务分解(Fork)成一系列子任务,子任务可以继续往下分解,当多个不同的子任务都执行完成后,可以将它们各自的结果合并(Join)成一个大结果,最终合并成大任务的结果:
在这里插入图片描述

使用示例

ForkJoinTask的抽象方法exec由RecursiveAction和RecursiveTask实现,很容易看出RecursiveAction和RecursiveTask的区别,前者没有result。

private static class SumTask extends RecursiveTask<Integer> {

        private static final int THRESHOLD = 20;

        private int arr[];
        private int start;
        private int end;

        public SumTask(int[] arr, int start, int end) {
            this.arr = arr;
            this.start = start;
            this.end = end;
        }

        /**
         * 小计
         */
        private Integer subtotal() {
            Integer sum = 0;
            for (int i = start; i < end; i++) {
                sum += arr[i];
            }
            System.out.println(Thread.currentThread().getName() + ": ∑(" + start + "~" + end + ")=" + sum);
            return sum;
        }

        @Override
        protected Integer compute() {

            if ((end - start) <= THRESHOLD) {
                return subtotal();
            } else {
                int middle = (start + end) / 2;
                SumTask left = new SumTask(arr, start, middle);
                SumTask right = new SumTask(arr, middle, end);

                //System.out.println(Thread.currentThread().getName());

                left.fork();
                right.fork();

                return left.join() + right.join();
            }
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {


        int[] arr = new int[100];
        for (int i = 0; i < 100; i++) {
            arr[i] = i + 1;
        }
        ForkJoinPool pool = new ForkJoinPool(1);
        ForkJoinTask<Integer> result = pool.submit(new SumTask(arr, 0, arr.length));
        System.out.println("最终计算结果: " + result.get());
        pool.shutdown();
    }

核心组件

F/J框架的实现非常复杂,内部大量运用了位操作和无锁算法,撇开这些实现细节不谈,该框架主要涉及4大核心组件:

  1. ForkJoinPool(线程池):
    ExecutorService的实现类,负责工作线程的管理、任务队列的维护,以及控制整个任务调度流程
  2. ForkJoinTask(任务):
    Future接口的实现类,fork是其核心方法,用于分解任务并异步执行;而join方法在任务结果计算完毕之后才会运行,用来合并或返回计算结果;
  3. ForkJoinWorkerThread(工作线程):
    Thread的子类,作为线程池中的工作线程(Worker)执行任务;
  4. WorkQueue(任务队列):
    任务队列,用于保存任务
ForkJoinPool

ForkJoinPool的主要工作如下:

  • 接受外部任务的提交
    1. 通过invoke方法提交的任务,调用线程直到任务执行完成才会返回,也就是说这是一个同步方法,且有返回结果;
    2. 通过execute方法提交的任务,调用线程会立即返回,也就是说这是一个异步方法,且没有返回结果;
    3. 通过submit方法提交的任务,调用线程会立即返回,也就是说这是一个异步方法,且有返回结果Future
  • 接受ForkJoinTask自身fork出的子任务的提交;
  • 任务队列数组(WorkQueue[])的初始化和管理;
  • 工作线程(Worker)的创建/管理。

ForkJoinPool构造器:

private ForkJoinPool(int parallelism, ForkJoinWorkerThreadFactory factory, UncaughtExceptionHandler handler,
                     int mode, String workerNamePrefix) {
    this.workerNamePrefix = workerNamePrefix;
    this.factory = factory;
    this.ueh = handler;
    //保存parallelism和mode信息,供后续读取
    this.config = (parallelism & SMASK) | mode;
    long np = (long) (-parallelism); 
    this.ctl = ((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK);

}
  • parallelism:默认值为CPU核心数,ForkJoinPool里工作线程数量与该参数有关,但它不表示最大线程数;
  • factory:工作线程工厂,默认是DefaultForkJoinWorkerThreadFactory,其实就是用来创建工作线程对象——ForkJoinWorkerThread;
  • handler:异常处理器;
  • config:保存parallelism和mode信息到int中,供后续读取;
  • ctl:线程池的核心控制字段,将下面信息按16bit为一组封装在一个long中。
    1. AC(48-63位): 表示现在正在运行的线程总数,初始值是并行值的相反数,总线程数没有达到阈值时它是一个负数
    2. TC(32-47位): 表示线程总量,初始值也是并行值的相反数,记录了我们一共开启了多少线程
    3. SS(16-31位): WorkQueue状态,第一位表示active的还是inactive,其余十五位表示版本号(对付ABA);
    4. ID(0-15位): 这里保存了一个WorkQueue在WorkQueue[]的下标,和其他worker通过字段stackPred组成一个TreiberStack。
  • AC和TC初始化时取的是parallelism负数,后续代码可以直接判断正负,为负代表还没有达到目标数量。
  • 线程池缺不了状态的变化,记录字段是runState,具体介绍在后面的“ForkJoinPool状态修改
ForkJoinTask

从Fork/Join框架的描述上来看,“任务”必须要满足一定的条件:

  • 支持Fork,即任务自身的分解
  • 支持Join,即任务结果的合并
ForkJoinWorkerThread

每个工作线程(Worker)都有一个自己的任务队列(WorkerQueue), 所以需要对一般的Thread做些特性化处理。ForkJoinWorkerThread 在构造过程中,会保存所属线程池信息和与自己绑定的任务队列信息。同时,它会通过ForkJoinPool的registerWorker方法将自己注册到线程池中。

线程池中的每个工作线程(ForkJoinWorkerThread)都有一个自己的任务队列(WorkQueue),工作线程优先处理自身队列中的任务(LIFO或FIFO顺序,由线程池构造时的参数
mode 决定),自身队列为空时,以FIFO的顺序随机窃取其它队列中的任务。

WorkQueue

任务队列(WorkQueue)是ForkJoinPool与其它线程池区别最大的地方,在ForkJoinPool内部,维护着一个WorkQueue[]数组,它会在外部首次提交任务)时进行初始化:

  1. 通过线程池的外部方法(submit、invoke、execute)提交任务时,
  2. 如果WorkQueue[]没有初始化,则会进行初始化;
  3. 根据数组大小和线程随机数(ThreadLocalRandom.probe)等信息,计算出任务队列所在的数组索引(这个索引一定是偶数)
  4. 如果索引处没有任务队列,则初始化一个,再将任务入队。

总结:通过外部方法提交的任务一定是在偶数队列,没有绑定工作线程。

WorkQueue作为ForkJoinPool的内部类,表示一个双端队列。双端队列既可以作为栈使用(LIFO),也可以作为队列使用(FIFO)。

ForkJoinPool的“工作窃取”正是利用了这个特点,当工作线程从自己的队列中获取任务时,默认总是以栈操作(LIFO)的方式从栈顶取任务;当工作线程尝试窃取其它任务队列中的任务时,则是FIFO的方式。减少冲突。ForkJoinPool构造中,可以指定线程池的同步/异步模式(mode参数),其作用就在于此。同步模式就是“栈操作”,异步模式就是“队列操作”,影响的就是工作线程从自己队列中取任务的方式
在这里插入图片描述

ForkJoinPool中的工作队列可以分为两类:

  • 有工作线程(Worker)绑定的任务队列:数组下标始终是奇数,称为task queue,该队列中的任务均由工作线程调用产生(工作线程调用FutureTask.fork方法);
  • 没有工作线程(Worker)绑定的任务队列:数组下标始终是偶数,称为submissions queue,该队列中的任务全部由其它线程提交(也就是非工作线程调用execute/submit/invoke或者FutureTask.fork方法)。
线程池调度示例

假设现在通过ForkJoinPool的submit方法提交了一个FuturetTask任务。


初始

初始状态下,线程池中的任务队列为空,workQueues == null,也没有工作线程:

在这里插入图片描述


外部提交FutureTask任务

此时会初始化任务队列数组WorkQueue[],大小为2的幂次,然后在某个槽位(偶数位)初始化一个任务队列(WorkQueue),并插入任务:
在这里插入图片描述
注意,由于是非工作线程通过外部方法提交的任务,所以这个任务队列并没有绑定工作线程。

之所以是2的幂次,是由于ForkJoinPool采用了一种随机算法(类似ConcurrentHashMap的随机算法),该算法通过线程池随机数(ThreadLocalRandom的probe值)和数组的大小计算出工作线程所映射的数组槽位,这种算法要求数组大小为2的幂次。


创建工作线程

首次提交任务后,由于没有工作线程,所以会创建一个工作线程,同时在某个奇数槽的位置创建一个与它绑定的任务队列,如下图:在这里插入图片描述


窃取任务

ForkJoinWorkThread_1会随机扫描workQueues中的队列,直到找到一个可以窃取的队列——workQueues[2],然后从该队列的base端获取任务并执行,并将base加1:
在这里插入图片描述
窃取到的任务是FutureTask,ForkJoinWorkThread_1最终会调用它的compute方法,该方法中会新建两个子任务,并执行它们的fork方法:

        @Override
        protected Integer compute() {

            if ((end - start) <= THRESHOLD) {
                return subtotal();
            } else {
                int middle = (start + end) / 2;
                SumTask left = new SumTask(arr, start, middle);
                SumTask right = new SumTask(arr, middle, end);

                left.fork();
                right.fork();

                return left.join() + right.join();
            }
        }
    }

工作线程ForkJoinWorkThread_1来调用FutureTask的fork方法,所以会将这两个子任务放入ForkJoinWorkThread_1自身队列中:在这里插入图片描述

从这里也可以看出,任务放到哪个队列,其实是由调用线程来决定的(根据线程探针值probe计算队列索引)。如果调用线程是工作线程,则必然有自己的队列(task queue),则任务都会放到自己的队列中;如果调用线程是其它线程(如主线程),则创建没有工作线程绑定的任务队列(submissions queue),然后存入任务。


新的工作线程

ForkJoinWorkThread_1调用两个子任务1和2的fork方法,除了将它们放入自己的任务队列外,如果 发现有空闲线程 ,会唤醒; 如果没空闲且线程没上线, 还会导致新增一个工作线程ForkJoinWorkThread_2。
在这里插入图片描述
ForkJoinWorkThread_2运行后会像ForkJoinWorkThread_1那样从其它队列窃取任务,从ForkJoinWorkThread_1队列的base端窃取一个任务(直接执行,并不会放入自己队列
在这里插入图片描述
窃取完成后,ForkJoinWorkThread_2会直接执行任务1,又回到了FutureTask子类的compute方法,假设此时又fork出两个任务——任务3、任务4。则重复上述步骤:窃取、执行、入队、join阻塞、返回。


自身队列的任务执行

如果没有空闲线程且线程数已上线,join方法会判断当前任务是否位于自己的队列的top,如果则执行。
在这里插入图片描述
ForkJoinWorkThread_1从栈顶开始执行并移除任务,先执行任务2并移除,再执行任务1
在这里插入图片描述

主要参考

Java多线程进阶(四三)—— J.U.C之executors框架:Fork/Join框架(1) 原理
Java多线程进阶(四四)—— J.U.C之executors框架:Fork/Join框架(2)实现
分析jdk-1.8-ForkJoinPool实现原理(上)

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值