什么是 Fork/Join 框架
Fork/Join 框架是 Java 7 提供的一个用于并行执行任务的框架,是一个把大任务分割成若干
个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
举个例子:如果要计算一个超大数组的和,最简单的做法是在一个线程里循环完成:
我们也可以将数组拆成两部分,分别由两个线程去处理,最后将处理结果再进行合并
如果拆成两部分后,每个子部分还是很大,我们可以继续拆分成四部分,用四个线程去处理
上边这个例子用到的就是分治法,分治法解题的一般步骤为:
- 分解,将要解决的问题划分成若干规模较小的同类问题;
- 求解,当子问题划分得足够小时,用较简单的方法解决;
- 合并,将子问题的解,逐层合并构成原问题的解。
Fork/Join 就是 Java 7 提供的一种根据分治法思想进行任务处理的一个并行框架。
Fork/Join 使用两个类来实现以上分治法的任务处理。
- ForkJoinTask :我们要使用 Fork/Join 框架,首先需要创建一个 ForkJoinTask 任务。ForkJoinTask 中有两个方法,fork() 方法用于分解任务,join() 方法会等待子任务执行完并得到其结果。通常情况下,我们不需要直接继承 ForkJoinTask 类,只需要继承它的子类,Fork/Join 框架提供了以下两个子类。
- RecursiveAction 用于没有返回值的任务
- RecursiveTask 用于有返回值的任务
- ForkJoinPool :ForkJoinPool 是用来执行 ForkJoinTask 任务的线程池。ForkJoinPool 线程池中维护着一些双端队列,分割的子任务会分别放进双端队列中,几个启动线程分别从双端队列里获取任务执行。
Fork/Join 框架使用示例
我们通过一个简单的需求来展示一下 Fork/Join 框架的使用,需求是:计算 1+2+3+…+19+20 的结果。
定义一个任务类,继承 RecursiveTask:
class SumTask extends RecursiveTask<Integer> {
/**
* 阈值,当数组长度小于等于此阈值时,就认为任务已经足够小了,可以执行计算
*/
private static final int THRESHOLD = 5;
private Integer[] array;
public SumTask(Integer[] array) {
this.array = array;
}
@Override
protected Integer compute() {
if(null == array){
return 0;
}
int sum = 0;
// 如果已经拆分的足够小了,则对子任务进行求解
if(array.length <= THRESHOLD){
for(int i : array){
sum += i;
}
System.out.println("线程:"+Thread.currentThread().getName()+" 对数组"+Arrays.toString(array)+" 进行计算,计算结果为["+sum+"]");
return sum;
}
// 拆分的还不够小,则继续进行拆分
int midIndex = array.length / 2;
SumTask sumTask1 = new SumTask(Arrays.copyOf(array, midIndex));
SumTask sumTask2 = new SumTask(Arrays.copyOfRange(array, midIndex, array.length));
sumTask1.fork();
sumTask2.fork();
int sum1 = sumTask1.join();
int sum2 = sumTask2.join();
// 将子任务结果进行合并
sum = sum1 + sum2;
System.out.println("线程:"+Thread.currentThread().getName()+"对子任务计算的结果进行合并:["+sum1+"] + ["+sum2+"] = ["+sum+"]");
return sum;
}
}
在 main 方法中模拟客户端的调用:
public static void main(String[] args) throws ExecutionException, InterruptedException {
Integer[] array = new Integer[20];
for(int i=0; i<20;){
array[i] = ++i;
}
// 创建一个 ForkJoinTask 任务
SumTask sumTask = new SumTask(array);
// 创建一个 ForkJoinPool 线程池,并将 ForkJoinTask 任务提交给线程池去执行
ForkJoinPool forkJoinPool = new ForkJoinPool(4);
Future<Integer> future = forkJoinPool.submit(sumTask);
// 异步获取结果
int sum = future.get();
System.out.println("最终计算结果为:"+sum);
}
执行 main 方法后输出如下:
线程:ForkJoinPool-1-worker-2 对数组[1, 2, 3, 4, 5] 进行计算,计算结果为[15]
线程:ForkJoinPool-1-worker-3 对数组[11, 12, 13, 14, 15] 进行计算,计算结果为[65]
线程:ForkJoinPool-1-worker-0 对数组[6, 7, 8, 9, 10] 进行计算,计算结果为[40]
线程:ForkJoinPool-1-worker-2对子任务计算的结果进行合并:[15] + [40] = [55]
线程:ForkJoinPool-1-worker-3 对数组[16, 17, 18, 19, 20] 进行计算,计算结果为[90]
线程:ForkJoinPool-1-worker-3对子任务计算的结果进行合并:[65] + [90] = [155]
线程:ForkJoinPool-1-worker-1对子任务计算的结果进行合并:[55] + [155] = [210]
最终计算结果为:210
双端队列 和 任务窃取算法
使用 Fork/Join 执行任务时,会将任务拆分成很多个较小的子任务,为了减少线程间竞争,把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列中的任务,线程和队列一一对应。但是,有的线程先把自己队列中的任务执行完,而其它线程对应的队列中还有任务等待处理,干完活的线程与其等着,不如去帮其它线程干活,于是就随机去其它队列中窃取一个任务来执行,这也就是 Fork/Join 的任务窃取机制。为了减少窃取任务线程和被窃取任务线程访问同一个队列时发生的竞争,通常会使用双端队列,窃取任务的线程永远从双端队列的尾部拿任务执行,而被窃取任务线程永远从双端队列的头部拿任务执行。