使用Demo
public class TestForkJoin {
public static void main(String[] args) {
int[] array = new int[10000];
for (int i = 0; i < 10000; i++) {
array[i] = i;
}
ArraySumTask task = new ArraySumTask(array, 0, 9999);
ForkJoinPool executor = new ForkJoinPool();
ForkJoinTask future = executor.submit(task);
try {
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class ArraySumTask extends RecursiveTask<Long> {
private final int[] array;
private final int begin;
private final int end;
private static final int THRESHOLD = 100;
public ArraySumTask(int[] array, int begin, int end) {
this.array = array;
this.begin = begin;
this.end = end;
}
@Override
protected Long compute() {
long sum = 0;
if (end - begin + 1 < THRESHOLD) { // 小于阈值, 直接计算
for (int i = begin; i <= end; i++) {
sum += array[i];
}
} else {
int middle = (end + begin) / 2;
ArraySumTask subtask1 = new ArraySumTask(this.array, begin, middle);
ArraySumTask subtask2 = new ArraySumTask(this.array, middle + 1, end);
subtask1.fork();
subtask2.fork();
long sum1 = subtask1.join();
long sum2 = subtask2.join();
sum = sum1 + sum2;
}
return sum;
}
}
分治算法思想
- 分治(divide and conquer),也就是把一个复杂的问题分解成相似的子问题,然后子问题再分子问题,直到问题分的很简单不必再划分了。然后层层返回子问题的结果,最终合并返回问题结果。
- 分治在算法上有很多应用,类似大数据的MapReduce,归并算法、快速排序算法等。JUC中的Fork/Join的并行计算框架类似于单机版的 MapReduce。
- 第一个阶段分解任务(fork),把任务分解为一个个小任务直至小任务可以简单的计算返回结果。
- 第二阶段合并结果(join),把每个小任务的结果合并返回得到最终结果。
Fork/Join框架
Fork/Join框架主要包含两部分:ForkJoinPool、ForkJoinTask。
- ForkJoinPool:治理分治任务的线程池。线程池中的每个工作线程都维护着一个工作队列(WorkQueue),这是一个双端队列(Deque),里面存放的对象是任务(ForkJoinTask)。
ForkJoinPool提供了3类外部提交任务的方法:invoke、execute、submit,它们的主要区别在于任务的执行方式上。- invoke:调用线程直到任务执行完成才会返回,也就是说这是一个同步方法,且有返回结果;
- execute:调用线程会立即返回,也就是说这是一个异步方法,且没有返回结果;
- submit:调用线程会立即返回,也就是说这是一个异步方法,且有返回结果(返回Future实现类,可以通过get获取结果)。
- WorkQueue:保存要执行的 ForkJoinTask
- ForkJoinWorkerThread:ForkJoinPool 内的 worker thread,执行 ForkJoinTask,内部有WorkQueue
- ForkJoinTask:可以声明以同步/异步方式进行执行的任务,有两个抽象子类:
- RecusiveAction:有返回值的任务
- RecusiveTask:没有返回值的任务
ForkJoinPool与ThreadPoolExecutor都是ExecutorService的实现类,其区别在于?
- ThreadPoolExecutor的线程池是只有一个任务队列的,而ForkJoinPool有多个任务队列。通过ForkJoinPool的invoke或submit或execute提交任务的时候会根据一定规则分配给不同的任务队列,并且任务队列是双端队列。
- 使用ForkJoinPool能够使用数量有限的线程来完成非常多的具有父子关系的任务,比如使用4个线程来完成超过200万个任务。但是,使用ThreadPoolExecutor时,是不可能完成的,因为ThreadPoolExecutor中的Thread无法选择优先执行子任务,需要完成200万个具有父子关系的任务时,也需要200万个线程,显然这是不可行的。
工作窃取机制
- 一般的线程池只有一个任务队列,但是对于Fork/Join框架来说,由于Fork出的各个子任务其实是平行关系,为了提高效率,减少线程竞争,将这些平行的任务放到不同的队列中去。
由于线程处理不同任务的速度不同,这样就可能存在某个线程先执行完了自己队列中的任务的情况,这时为了提升效率,我们可以让该线程去“窃取”其它任务队列中的任务,这就是所谓的工作窃取算法。
“工作窃取”的示意图如下,当线程1执行完自身任务队列中的任务后,尝试从线程2的任务队列中“窃取”任务:
- 工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。