前言
从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大核心组件:
- ForkJoinPool(线程池):
ExecutorService的实现类,负责工作线程的管理、任务队列的维护,以及控制整个任务调度流程 - ForkJoinTask(任务):
Future接口的实现类,fork是其核心方法,用于分解任务并异步执行;而join方法在任务结果计算完毕之后才会运行,用来合并或返回计算结果; - ForkJoinWorkerThread(工作线程):
Thread的子类,作为线程池中的工作线程(Worker)执行任务; - WorkQueue(任务队列):
任务队列,用于保存任务
ForkJoinPool
ForkJoinPool的主要工作如下:
- 接受外部任务的提交
- 通过invoke方法提交的任务,调用线程直到任务执行完成才会返回,也就是说这是一个同步方法,且有返回结果;
- 通过execute方法提交的任务,调用线程会立即返回,也就是说这是一个异步方法,且没有返回结果;
- 通过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中。
- AC(48-63位): 表示现在正在运行的线程总数,初始值是并行值的相反数,总线程数没有达到阈值时它是一个负数
- TC(32-47位): 表示线程总量,初始值也是并行值的相反数,记录了我们一共开启了多少线程
- SS(16-31位): WorkQueue状态,第一位表示active的还是inactive,其余十五位表示版本号(对付ABA);
- 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[]数组,它会在外部首次提交任务)时进行初始化:
- 通过线程池的外部方法(submit、invoke、execute)提交任务时,
- 如果WorkQueue[]没有初始化,则会进行初始化;
- 根据数组大小和线程随机数(ThreadLocalRandom.probe)等信息,计算出任务队列所在的数组索引(这个索引一定是偶数)
- 如果索引处没有任务队列,则初始化一个,再将任务入队。
总结:通过外部方法提交的任务一定是在偶数队列,没有绑定工作线程。
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实现原理(上)》