java中Fork-Join框架原理及应用
一、使用场景
大任务拆解成多个子任务,子任务还可以继续拆解成更小的子任务,最后将这些最小的子任务用多个线程并行执行,然后合并执行结果,例如,对超过1000万个元素的数组进行排序。
需求:有一个大数据量的List,现在需要根据List中每个对象model的id属性去另一个接口(接口只能接受一个id参数)获取这个对象的另外一个属性并set到这个对象中,很显然我们需要循环这个list,如果数据量很大,程序会变得非常慢,于是我们考虑用Fork-Join框架来处理(比线程池简单,不需要手动开启关闭线程池)。
二、基本思想
ForkJoin模型利用了分治算法的思想,将大任务不断拆解,多线程执行,最后合并结果。它的本质是一个线程池。
二、工作逻辑
每一个工作线程维护一个本地的双端队列用来存放任务。线程在运行的过程中产生新的任务(通常是因为调用了 fork())时,会放入工作队列的队尾,并且工作线程在处理自己的工作队列时,从队尾取出任务来执行。当某个工作线程的本地队列为空时,它会尝试窃取一个任务,也叫工作窃取(可能是来自于刚刚提交到 pool 的任务,也可能是来自于其他工作线程的工作队列),窃取其他线程的工作队列的任务时,从队首取出,加到自己的队列中,然后执行下面两步:
- 如果任务足够小就直接执行。
- 否则将任务拆分成更小的子任务。
从上面的过程可以看出,Fork join并不是预先拆分所有任务,而是在执行时动态的决定拆分。
ForkJoin有三个比较重要的方法(操作)
1. fork:开启一个新线程(或是重用线程池内的空闲线程),将任务交给该线程处理。
2. join:等待子任务的处理线程处理完毕,获得返回值。
3. compute:拆解并执行任务
代码:
ForkJoinPool forkJoinPool = new ForkJoinPool(4);
QTask task = new QTask(0, list.size(),list);
ForkJoinTask<List<Model>> submit = forkJoinPool.submit(task);
list2=(ArrayList<Model>) submit.get();
forkJoinPool.shutdown();
QTask.java
import java.util.List;
import java.util.concurrent.RecursiveTask;
public class QTask extends RecursiveTask {
/**
*
*/
private static final long serialVersionUID = 6253771003381008573L;
// 分片阈值
public static final int threshold = 100;
private int start;
private int end;
private List<Model> list;
public QTask(int start, int end, List<Model> list) {
this.start = start;
this.end = end;
this.list = list;
}
/**
*/
@Override
protected List<Model> compute() {
String threadName = Thread.currentThread().getName();
// 小于阈值直接执行
if (end - start <= threshold) {
for (int i = start; i < end; i++) {
list.get(i).setTitle("测试"+i);
}
} catch (NumberFormatException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (OspException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} else {
// 递归拆解任务
int middle = (start + end) / 2;
QTask leftTask = new QTask(start, middle,list);
QTask rightTask = new QTask(middle, end,list);
invokeAll(leftTask, rightTask);
// 等待计算完成并返回计算结果。
leftTask.join();
rightTask.join();
}
System.err.println(threadName + "==========start:" + start+"====================end:"+end);
return list;
}
}
为什么不用fork而用invokeAll?
这是因为执行compute()方法的线程本身也是一个Worker线程,当对两个子任务调用fork()时,这个Worker线程就会把任务分配给另外两个Worker,但是它自己却停下来等待不干活了(监工)!这样就白白浪费了Fork/Join线程池中的一个Worker线程,导致了4个子任务至少需要7个线程才能并发执行。
而从invokeAll()方法的源码中发现,invokeAll的N个任务中,其中N-1个任务会使用fork()交给其它线程执行,但是,它还会留一个任务自己执行,这样,就充分利用了线程池,保证没有空闲的不干活的线程。
那么它和线程池有什么区别呢?
假设有一个包含4个线程的线程池,当一个任务到达时,我们把任务放到队列里。当一个线程空闲时,这个线程就从队头取一个待执行的任务并且执行,这种方式在任务大小类似的情况下效果很好,但是当任务大小不一致的时候呢?比如,有四个任务ABCD,其中有A任务所需要的执行时间非常长,而BCD任务是比较简单的,假设线程1拿到了A任务,线程234拿到了其余的任务,于是,线程234早早的就会执行完,处在空闲的状态,而A任务比较复杂,所以线程1则会执行较长时间。
故而,与线程池模型相比,ForkJoin模型有以下两个优点:
- 自动完成任务到线程的映射,我们只关心任务的拆解机制、设置阈值以及小任务的执行。
- 将大任务拆解,合理利用线程资源,提高效率,注意,子任务被分配到不同的核上执行时,效率最高。