持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第8天,点击查看活动详情
分治思想
Fork/Join框架应用“分而治之”的思想,即:
通过fork()拆分子任务,通过join()获取子任务的计算结果。
图中是一个二路三层的Fork-Join,对于K路N层的Fork-Join会产生个最小计算任务
使用ForkJoin实现的归并排序如下所示
public class MergeSortTask extends RecursiveTask<int[]> {
private int array[];
private int n;
public MergeSortTask(int[] array, int n) {
this.array = array;
this.n = n;
}
@Override
protected int[] compute() {
if (array.length <= n) {
Arrays.sort(array);
return array;
} else {
MergeSortTask task1 = new MergeSortTask(Arrays.copyOf(array, array.length / 2), n);
task1.fork();
MergeSortTask task2 = new MergeSortTask(Arrays.copyOfRange(array, array.length / 2, array.length), n);
task2.fork();
return merge(task1.join(), task2.join());
}
}
private static int[] merge(int array1[], int array2[]) {
int length1 = array1.length;
int length2 = array2.length;
int[] result = new int[length1 + length2];
for (int index = 0, index1 = 0, index2 = 0; index < result.length; index++) {
int value1 = index1 >= length1 ? Integer.MAX_VALUE : array1[index1];
int value2 = index2 >= length2 ? Integer.MAX_VALUE : array2[index2];
if (value1 < value2) {
index1++;
result[index] = value1;
} else {
index2++;
result[index] = value2;
}
}
return result;
}
}
Fork/Join和ThreadPoolExecutor的区别
和ThreadPoolExecutor中的“生产/消费”模式不同,Fork/Join处理的分治任务是有着依赖关系的,因此让每个线程处理自己对应的任务队列。
在Fork/Join框架中,我们将任务分为外部提交的和自己fork的,外部提交任务放到偶数队列中,自己fork的放到奇数队列中,只有奇数队列才有线程处理任务,偶数队列中的任务靠“工作窃取”消耗。
工作窃取
双端队列支持先进先出和先进后出两种模式,当有线程空闲时会从其他线程的队列中窃取任务,例如先进后出的栈模式中,消费从top取出,工作窃取从base端取出,这样保证了优先将大任务拆分成小任务。
非阻塞的Join操作
和传统线程不一样的是,Join操作不会阻塞线程。Fork/Join框架通过在执行线程和任务之间引入一个“中间层”,允许将阻塞的任务放在一边并且等所有它所依赖的子任务全部处理完成后,再去处理这个因为子任务而阻塞的任务。
也就是说,假设“任务1”依赖“任务5”(其中,任务5是由任务1创建的子任务),那么任务1就被放到一边,去执行“任务5”。