第四节 ForkJoin框架

4.1 ForkJoin框架

1. 什么是ForkJoin框架 适用场景

虽然目前处理器核心数已经发展到很大数目,但是按任务并发处理并不能完全充分的利用处理器资源,因为一般的应用程序没有那么多的并发处理任务。基于这种现状,考虑把一个任务拆分成多个单元,每个单元分别得到执行,最后合并每个单元的结果。

Fork/Join框架是JAVA7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干小任务,最终汇总每个小任务结果得到大任务结果的框架。

2.工作窃取算法(work-stealing)

一个大任务拆分成多个小任务,为了减少线程间的竞争,把这些子任务分别放到不同的队列中,并且每个队列都有单独的线程来执行队列里的任务,线程和队列一一对应。

但是会出现这样一种情况:A线程处理完了自己队列的任务,B线程的队列里还有很多任务要处理。

A是一个很热情的线程,想过去帮忙,但是如果两个线程访问同一个队列,会产生竞争,所以A想了一个办法,从双端队列的尾部拿任务执行。而B线程永远是从双端队列的头部拿任务执行。

注意:线程池中的每个线程都有自己的工作队列(PS,这一点和ThreadPoolExecutor不同,ThreadPoolExecutor是所有线程公用一个工作队列,所有线程都从这个工作队列中取任务),当自己队列中的任务都完成以后,会从其它线程的工作队列中偷一个任务执行,这样可以充分利用资源。

工作窃取算法的优点:

利用了线程进行并行计算,减少了线程间的竞争。

工作窃取算法的缺点:

1、如果双端队列中只有一个任务时,线程间会存在竞争。

2、窃取算法消耗了更多的系统资源,如会创建多个线程和多个双端队列。

3.主要类

l ForkJoinTask:

使用该框架,需要创建一个ForkJoin任务,它提供在任务中执行fork和join操作的机制。一般情况下,我们并不需要直接继承ForkJoinTask类,只需要继承它的子类,它的子类有两个:

² RecursiveAction:用于没有返回结果的任务。

² RecursiveTask:用于有返回结果的任务。

l ForkJoinPool:

任务ForkJoinTask需要通过ForkJoinPool来执行。

l ForkJoinWorkerThread:

ForkJoinPool线程池中的一个执行任务的线程。

4.2 ForkJoin框架示例

【示例8】使用ForkJoin框架计算 1+2+3......+n = ?

public class SumTask extends RecursiveTask {
private int start;
private int end;
private final int step = 2000000;//最小拆分成几个数相加 public SumTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
long sum = 0;
if(end - start <= step ){
//小于5个数,直接求和 for (int i = start; i <=end; i++) {
sum+=i;
}
}else{
//大于5个数,分解任务 int mid = (end + start)/2;
SumTask leftTask = new SumTask(start,mid);
SumTask rightTask = new SumTask(mid+1,end);
//执行子任务 leftTask.fork();
rightTask.fork();
//子任务,执行完,得到执行结果 long leftSum = leftTask.join();
long rightSum = rightTask.join();
sum = leftSum+rightSum;
}
return sum;
}
public static void main(String[] args)
throws ExecutionException, InterruptedException {
//如果多核CPU,其实是一个一直使用,其他闲置;怎么办,多线程解决; //但是涉及到任务的拆分与合并等众多细节,不要紧,//现在使用ForkJoin框架,可以较轻松解决; long start = System.currentTimeMillis();
long sum = 0;
for(int i=0;i<=1000000000;i++){
sum +=i;
}
System.out.println(sum);
long end = System.currentTimeMillis();
System.out.println("for:"+(end - start));
//使用ForkJoin框架解决 //创建一个线程池 ForkJoinPool pool = new ForkJoinPool();
//定义一个任务 SumTask sumTask = new SumTask(1,1000000000);
//将任务交给线程池 start = System.currentTimeMillis();
Future future = pool.submit(sumTask);
//得到结果并输出 Long result = future.get();
System.out.println(result);
end = System.currentTimeMillis();
System.out.println("pool:"+(end - start));
}
}

可以看出,使用了 ForkJoinPool 的实现逻辑全部集中在了 compute() 这个函数里,仅用了很少行就实现了完整的计算过程。特别是,在这段代码里没有显式地“把任务分配给线程”,只是分解了任务,而把具体的任务到线程的映射交给了 ForkJoinPool 来完成。

本节作业

1. 说明ForkJoin所采用的工作窃取算法(双端队列 偷偷做好事)

2. 使用ForKJoin计算1+2+3……..+n的和