一、概述
Fork-Join 框架是JUC作者Doug Lea在Java7中为jvm设计提供了一个并行执行任务的框架。其主旨是将大任务拆分成若干个小任务,再并行对这些小任务进行计算处理,汇总所有小任务的处理结果进行合并得到大任务的最终结果。FrokJoinPool在Java7是基于Fork-Join框架实现的线程池。
二、原理
FrokJoinPool是Java7开始jvm提供的一种用于并行执行任务的线程池,核心思想是将大任务拆分成多个小任务(fork),然后再对多个小任务并行处理汇总到一个结果上(join),非常像MapReduce处理原理。同时也提供了线程池基本功能,支持最大并发线程数、支持线程池的停止和监控等功能。ForkJoinPool和ThreadPoolExecutor都是AbstactExecutorService的子类。主要引入了“分治”和 “工作窃取”机制,能够更好的使用多CPU,提高计算机处理性能。
2.1 分治机制
ForkJoinPool和MapReduce一样都采用了分治算法。算法的基本思想是将一个规模为N的问题拆分为K个规模较小的子问题,这些子问题相互独立且与原问题的性质相同,求出子问题的解后,将这些解合并,就可以得到原文的解。是一种分目标完成的程序算法。跟二分法非常相似,二分法(Binary Search)在检索时常常用到,他可以就将检索的时间复杂度从O(n) 降到O(log n)。对应到FrokJoinPool对问题的处理也是如此。ForkJoinPool基本拆分原理如下:
以上只是Frok-Join简化版处理任务流程。在实际工作中可能比这个更加复杂。但任务拆分原理基本相同。将一个大任务通过fork方法不断地拆分,直到可以计算为止,再通过join方法对每个小任务的结果进行合并,这样逐次递归最终得到大任务的结果。这就是FrokJoinPool线程池的分治机制。
2.2 工作窃取机制(work-stealing)
工作窃取是指当某个线程的工作队列(WorkQueue)中没有可执行的任务时,从其他线程的工作队列(WorkQueue)中窃取任务来执行,以充分利用线程的处理能力,减少线程因获取不到任务而造成的空闲浪费。当ThreadPoolExecutor还在用单个队列存放任务时,FrokJoinPool已经分配了与线程数相等的工作队列(WorkQueue)。
常见普通队列都是采用FIFO(先进先出)方式工作,而工作队列(WorkQueue)为了实现工作窃取机制采用了双端队列模式,即当前线程获取任务采用FILO(先进后出)队尾获取,其他线程窃取任务采用FIFO(先进先出)队头获取,这是为了尽量避免线程竞争。
工作原理如图:
线程worker1、worker2以及worker3都有自己对应的工作队列TaskQueue1、TaskQueue2和TaskQueue3,线程往对应的队列中pushing和poping任务都是从队尾开始操作。当TaskQueue3队列中没有任务时,线程worker3就会从其他队列中stealing任务,这样worker3线程就不会因无任务而空闲,该机制充分利用了线程资源。
三、FrokJoinPool使用
3.1 实例化
java提供了FrokJoinPool来支持将一个任务拆分多个子任务并行处理,并将多个子任务的结果集合并成总的结果。FrokJoinPool提供了两个常用的构造方法
// 创建一个指定线程数的ForkJoinPool线程池
public ForkJoinPool(int parallelism);
//通过Runtime.getRuntime().availableProcessors()计算出当前计算机CPU核数作为线程数实例化线程池
public ForkJoinPool() ;
注意:当实际工作中任务拆分次数大于线程池线程数时,也不会再创建新的线程和工作队列。
3.2 任务进行Fork-Join
ForkJoinPool线程池实例化后通过submit(ForkJoinTask task)或invoke(ForkJoinTask task)两个方法来执行任务。其中任务ForkJoinTask是一个可以并行、合并的任务,FrokJoinTask是抽象类,虽然有很多子类,我们常用带返回值的RecursiveTask 和不带返回值的RecursiveAction抽象子类。
抽象子类:
RecursiveTask:有返回值的任务抽象子类
RecursiveAction:没有返回值的任务抽象子类。
根据业务需要选择继承不同的抽象子类,通过重写compute()方法使用FrokJoinTask.fork()对任务进行拆分,使用ForkJoinTask.join()对结果集进行合并。
3.3 代码实例
RecursiveTask
实例:计算从1到10000累加后的总和
/**
* 继承RecursiveTask类,重写compute方法进行任务拆解
*/
public class SumTask extends RecursiveTask<Long> {
/**
* 每个"小任务"最多100个数
*/
private static final int MAX = 100;
private final int start;
private final int end;
public SumTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
long sum = 0;
// 当end-start的值小于MAX时候,开始打印
if((end - start) < MAX) {
for (int i = start; i <= end; i++) {
sum += i;
}
}else {
System.err.println("=====任务分解======");
// 将大任务分解成两个小任务
int middle = (start + end) / 2;
SumTask subTask1 = new SumTask(start, middle);
SumTask subTask2 = new SumTask(middle + 1, end);
// 并行执行两个小任务
subTask1.fork();
subTask2.fork();
// 把两个小任务累加的结果合并起来
sum = subTask1.join() + subTask2.join();
}
return sum;
}
}
/**
* FrokJoinPool测试类
*/
public class FrokJoinPoolTest{
public static void main(String[] args){
// 创建线程数为10的线程池
ForkJoinPool executor = new ForkJoinPool(10);
// 创建执行计算任务
SumTask task = new SumTask(1, 10000);
//将任务提交到线程池
ForkJoinTask<Long> future = executor.submit(task);
//打印计算结果
System.out.println("result:" + future.get());
}
}
RecursiveAction
实例:打印0-1000的数值。
public class RaskDemo extends RecursiveAction {
/**
* 每个"小任务"最多只打印20个数
*/
private static final int MAX = 20;
private int start;
private int end;
public RaskDemo(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected void compute() {
//当end-start的值小于MAX时,开始打印
if((end-start) < MAX) {
for(int i= start; i<end;i++) {
System.out.println(Thread.currentThread().getName()+"i的值"+i);
}
}else {
// 将大任务分解成两个小任务
int middle = (start + end) / 2;
RaskDemo left = new RaskDemo(start, middle);
RaskDemo right = new RaskDemo(middle, end);
left.fork();
right.fork();
}
}
}
-------------------------------------------------------------------------------
public static void main(String[] args) throws Exception{
// 通过Runtime.getRuntime().availableProcessors()计算创建线程数为计算机CPU核数的线程池
ForkJoinPool forkJoinPool = new ForkJoinPool();
// 提交可分解的PrintTask任务
forkJoinPool.submit(new RaskDemo(0, 1000));
//阻塞当前线程直到 ForkJoinPool 中所有的任务都执行结束
forkJoinPool.awaitTermination(2, TimeUnit.SECONDS);
// 关闭线程池
forkJoinPool.shutdown();
}
运行结果
从上面结果来看,ForkJoinPool启动了四个线程来执行这个打印任务,我的计算机的CPU是四核的。大家还可以看到程序虽然打印了0-999这一千个数字,但是并不是连续打印的,这是因为程序将这个打印任务进行了分解,分解后的任务会并行执行,所以不会按顺序打印。
3.4 代码执行分析
对继承RecursiveTask类的代码执行进行debug跟踪分析FrokJoinPool源码执行流程基本可以分为三部分submit、fork以及join。
ForkJoinPool.submit()
submit()方法执行流程基本分为4个步骤:
1、创建工作队列(WorkQueue)数组
2、创建具体队列workQueue0
3、将提交的大任务-task放入workQueue0
4、创建一个FrokJoinWorkerThread的线程0
5、线程0获取大任务-task,并执行compute()方法进行任务拆分
ForkJoinTask.frok()
1、执行任务1.fork()方法,fork()将任务1放入wq0队列
2、新建线程1和wq1队列
3、执行任务2.fork()方法,过程与任务1时相同,但线程1在对应的wq1队列未找到任务,那就从别的wq0队列中获取任务1执行(工作窃取过程)
4、线程1、线程2工作窃取到任务后执行并进行任务拆分(过程同任务1、任务2),递归拆分直到执行累加流程不再拆分任务。
5、当任务拆分后子任务大于线程数时就不在创建新的线程和队列,而是通过现有线程采用 “工作窃取” 来执行的任务
7、当前线程获取对应队列任务采用的是FILO(先进后出)机制,而工作窃取其他队列采用的是FIFO(先进先出)机制。这是为了避免竞争冲突
FrokJoinTask.join()
1、执行任务1.join方法,让当前线程0进入等待状态,直到FrokJoinTask任务完成并返回结果。
2、join通过内部doJoin等方法来判断任务的状态,若状态为已完成则返回任务结果,若状态为信号状态表示任务正在执行或等待执行
3、子任务执行完的结果都统一放到一个队列中,一旦所有子任务完成,她们的结果会被合并(RecursiveTask),或简单表示任务已完成(RecursiveAction)。对于RecursiveTask合并结果是在join方法返回之前完成的。
4、对join的任务1和任务2合并的结果进行逻辑处理
通过以上FrokJoinPool执行源码分析,我们可以知道以下几点:
1、FrokJoinPool首先会创建一个WorkQueue工作队列数组,每一个线程都有对应WorkQueue工作队列。
2、WorkQueue工作队列是双端队列,当前线程从尾端添加和获取任务,其他线程从头部窃取任务,减少竞争冲突。
3、通过ForkJoinTask.frok对大任务进行拆分成子任务,若子任务还是很多,则继续拆分直到拆分的子任务可以直接执行逻辑。每次拆分的子任务都会新创建一个线程和对应的WorkQueue工作队列。创建的线程数量不能超过FrokJoinPool的线程数。
4、拆分后的子任务放到线程对应的WorkQueue中,当前线程执行队列中的任务。
5、能够充分利用利用多个CPU提供计算资源来完成一个复杂的任务,提高处理效率。
三、总结
本文介绍了FrokJoinPool的原理、使用方式以及执行流程源码解析。我们知道了Frok-Join框架的核心思想。FrokJoinPoo是l实现Frok-Join框架引入的线程池。在日常工作中可以使用FrokJoinPool线程池可以很好的处理大任务,降低处理时间提高运行效率。在java8中的Stream中广泛使用。