“分而治之”的思想,就是把一个大的问题转换成一个个小的问题,然后逐个去解决,最后将各个部分的解组成整个问题的解。Fork Join模式没有master角色,都是Worker,将大的任务切成小的任务,一直到任务规模足够小。
Fork Join的原理
使用了一个无限队列来保存需要执行的任务,而线程的数量则是通过构造函数传入,如果没有向构造函数中传入指定的线程数量,那么当前计算机可用的CPU数量会被设置为线程数量作为默认值,ForkJoinPool能够使用相对较少的线程来处理大量的任务。 如果使用了ThreadPoolExecutor时,那么就会因为ThreadPoolExecutor无法向任务队列中再添加一个任务并在等待该任务完成之后再继续执行。而使用ForkJoinPool就能够解决这个问题,它能够让其中的线程创建新的任务,并挂起当前的任务,此时线程就能够从队列中选择子任务执行。
工作窃取算法
一个比较大的任务被拆分成多个小的任务,并且这些任务并不相互依赖,为了减少线程之间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来完成这个任务,线程和任务队列一一对应。但是如果有的线程里面的工作已经完成了,那么就会空着,这个时候还不如让别的线程中等待的任务来进入已经完成的线程中继续执行。
优点:充分利用线程进行并行计算,并减少了线程间的竞争。
缺点:
- 只能使用Fork和join操作来进行同步机制,如果使用了其他同步机制,则再同步操作时,工作线程就不能执行其他任务
- 不应该绑定I/O操作
- 任务不能抛出检查异常,必须通过必要的代码来检查异常
if 小任务
直接进行任务
else
大任务切成小任务进行执行
ForkJoinTask类
- RecursiveAction:无返回值的任务,实现Callable
- RecursiveTask:有返回值的任务,实现Runnable
- CountedCompleter:完成任务后将触发其他任务,执行后会触发执行一个自定义的钩子函数
例子
@Slf4j
public class ForkJoinTaskExample extends RecursiveTask<Integer> {
public static final int threshold = 2;
private int start;
private int end;
public ForkJoinTaskExample(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
//如果任务足够小就计算任务
boolean canDirect = end - start <= threshold;
if (canDirect) {
for (int i = start; i <= end; i++) {
sum = sum + i;
}
} else {
int mid = (start + end) / 2;
ForkJoinTaskExample leftTask = new ForkJoinTaskExample(start, mid);
ForkJoinTaskExample rightTask = new ForkJoinTaskExample(mid + 1, end);
// 执行任务
leftTask.fork();
rightTask.fork();
// 等待任务执行结束后合并其结果
Integer left = leftTask.join();
Integer right = rightTask.join();
sum = left + right;
}
return sum;
}
public static void main(String[] args) {
ForkJoinPool forkjoinPool = new ForkJoinPool();
//生成一个计算任务,计算1+2+3+4
ForkJoinTaskExample task = new ForkJoinTaskExample(1, 100);
//执行一个任务
Future<Integer> result = forkjoinPool.submit(task);
try {
log.info("result:{}", result.get());
} catch (Exception e) {
log.error("exception", e);
}
}
}