深入理解并发编程之线程池Fork Join
一、什么是Fork Join
或许初级程序员不知道什么是Fork Join,我们先简单的举个例子来说明下:
假设我们需要群发10W封邮件,我们可能采用哪种方法呢?有人可能说使用线程或者线程池,每个线程发送1W封邮件,这样做肯定是没问题的,假设我们不是发邮件呢,可能是操作10W条复杂数据处理,而且处理过程根据数据情况处理的复杂度不同,这样就是10W条执行时间不同的数据,我们使用了10个线程,这就出现了个小问题,如果前面几万的数据比较简单处理的快,后面的几万条数据复杂处理时间是前面几万数据的3倍时间吧,那么我们前面几个线程就会先执行完毕,最终一直在等待后面的线程执行结束,有没有更好的办法让他们尽可能去平均分配时间执行呢?这就需要用到了Fork Join。
Fork Join是Java7提供的并行执行任务的框架,是一个把大任务分割成若干小任务,小任务可以继续不断拆分n多个小任务,最终汇总小任务的结果得到大任务结果的框架。
二、举例说明下Fork Join的用法
我们用3个线程的Fork Join线程池求1到10W的和来简单的看一下:
public class ForkJoinTest extends RecursiveTask<Long> {
private Long start; //计算的开始值
private Long end; //计算的最终值
private Long max = 100L; //计算区间的最大差值
private static AtomicInteger worker1 = new AtomicInteger(0); //分别记录每个线程执行次数
private static AtomicInteger worker2 = new AtomicInteger(0);
private static AtomicInteger worker3 = new AtomicInteger(0);
public ForkJoinTest(Long start, Long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
Long sum = 0L;
//如果计算的区间小于100直接计算
if (end - start < max) {
if ("ForkJoinPool-1-worker-1".equals(Thread.currentThread().getName())) {
worker1.incrementAndGet(); //如果线程1执行就++
System.out.println("worker1:" + worker1.get());
}
if ("ForkJoinPool-1-worker-2".equals(Thread.currentThread().getName())) {
worker2.incrementAndGet(); //如果线程2执行就++
System.out.println("worker2:" + worker2.get());
}
if ("ForkJoinPool-1-worker-3".equals(Thread.currentThread().getName())) {
worker3.incrementAndGet(); //如果线程3执行就++
System.out.println("worker3:" + worker3.get());
}
for (Long i = start; i <= end; i++) {
sum += i;
}
} else {
// 如果计算的区间>=100,分两半继续分直到分的区间小于100为止
Long l = (start + end) / 2;
ForkJoinTest left = new ForkJoinTest(start, l); //执行前面的一半
//Fork()方法 Fork()方法类似于Thread.start(),但是它并不立即执行任务,而是将任务放入工作队列中,拆分子任务。
left.fork();
ForkJoinTest rigt = new ForkJoinTest(l + 1, end); //执行后面的一半
rigt.fork();
try {
sum = left.get() + rigt.get(); //每一半的最终结果加起来
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
return sum;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinTest forkJoinTest = new ForkJoinTest(1L, 10000L);
ForkJoinPool forkJoinPool = new ForkJoinPool(3); //线程池有3个线程
ForkJoinTask<Long> submit = forkJoinPool.submit(forkJoinTest);
System.out.println("执行结果为:" + submit.get());
}
}
可以看一下计算结果:
不考虑线程的话我们仔细看一下上面代码的逻辑,其实就是一个递归操作,我们一直往下递归把10000拆分成每部分都区间小于100的多个部分进行计算,然后把最后的和汇总起来。其实Fork Join就是使用多线程的递归,首先设置个预期值,如果在预期值内直接执行,如果不在预期值内则继续递归,往下继续递归任务。
Fork Join继承的有两个类,RecursiveTask是有返回值的,RecursiveAction是没有返回值的。
Fork Join有两个核心方法:
- fork(): 用于将新创建的子任务放入当前线程的work queue队列中,这个概念类似于线程池,fork()方法会把任务放入工作队列中,子线程中使用invokeAll()比fork()效率高。
- join(): 用于让当前线程阻塞,直到对应的子任务完成运行并返回执行结果。
Fork Join有三种提交任务的方式:
- 通过invoke方法提交的任务,调用线程直到任务执行完成才会返回,也就是说这是一个同步方法,且有返回结果。
- 通过execute方法提交的任务,调用线程会立即返回,也就是说这是一个异步方法,且没有返回结果。
- 通过submit方法提交的任务,调用线程会立即返回,也就是说这是一个异步方法,且有返回结果(返回Future实现类,可以通过get获取结果。
内容来源:蚂蚁课堂