分支/合并框架 是以递归的方式将可以并行的任务切割成更小的任务,然后将每个任务的结果进行合并来生成最终的结果。它是ExecutorService接口的实现,子任务在ForkJoinPool中执行。
要把任务提交到ForkJoinPool 可以通过继承两个类:一个是没有返回结果的 RecursiveAction,另一个RecursiveTask<R>有返回结果,并且R是返回结果的类型。如果要定义RecursiveTask,只要实现其唯一的抽象方法 compute。这个方法实现任务拆分的逻辑,以及拆分到最小后,每一个任务的处理逻辑。
任务的拆分如图
二 代码实例
public class MyForkAndJoinTest extends RecursiveTask<Long> {
private final long[] numbers;
private final int start;
private final int end;
//阈值:拆分用
public static final int IDEND = 10000;
public MyForkAndJoinTest(long[] numbers){
this(numbers,0,numbers.length);
}
public MyForkAndJoinTest(long[] numbers, int start, int end) {
this.numbers = numbers;
this.start = start;
this.end = end;
}
//重新方法:实现拆分逻辑,以及单个任务的逻辑
@Override
protected Long compute() {
int le = end - start;
if(le<=IDEND){
//顺序执行任务
return getSumFact();
}
MyForkAndJoinTest leftTask = new MyForkAndJoinTest(numbers,start,start+le/2);
leftTask.fork();//将任务放入ForkJoinPool中,异步执行
MyForkAndJoinTest rightTask = new MyForkAndJoinTest(numbers,le/2+start,end);
Long result = rightTask.compute();//执行第二个任务
Long leftR = leftTask.join();//读取结果
return result+leftR;
}
public Long getSumFact(){
Long sum = 0L;
for (int i = start; i <end ; i++) {
sum +=numbers[i];
}
return sum;
}
public static long getSumToN(long n){
long[] nums = LongStream.rangeClosed(0,n).toArray();
ForkJoinTask<Long> tt = new MyForkAndJoinTest(nums);
return new ForkJoinPool().invoke(tt);
}
}
三 工作窃取机制
分出大量的小任务一般来说都是一个好的选择。这是因为,理想情况下,划分并行任务应该让每个任务都用完全相同的时间完成,让所有的CPU 内核都同样繁忙。不幸的是,实际中个子任务所花的时间可能天差地别,要么是因为划分策略效率低,要么是有不可预知的原因,比磁盘访问慢,或是需要和外部服务协调执行。
分支/合并框架工程用一种称为工作窃取(work stealing)的技术来解决这个问题。在实际用中,这意味着这些任务差不多被平均分配到ForkJoinPool中的所有线程上。每个线程都为配给它的任务保存一个双向链式队列,每完成一个任务,就会从队列头上取出下一个任务开始行。基于前面所述的原因,某个线程可能早早完成了分配给它的所有任务,也就是它的队列已空了,而其他的线程还很忙。这时,这个线程并没有闲下来,而是随机选了一个别的线程,从列的尾巴上“偷走”一个任务。这个过程一直继续下去,直到所有的任务都执行完毕,所有的列都清空。这就是为什么要划成许多小任务而不是少数几个大任务,这有助于更好地在工作线之间平衡负载。