1 Fork/Join Framework介绍
Fork/Join框架是在JDK1.7版本中被Doug Lea引入的,Fork/Join计算模型旨在充分利用多核CPU的并行运算能力,将一个复杂的任务拆分(fork)成若干个并行计算,然后将结果合并(join)
在JDK中,Fork/Join框架的实现为ForkJoinPool及ForkJoinTask等,虽然这些API在日常工作中的使用并不是非常频繁,但是在很多更高一级的JVM开发语言(比如,Scala、Clojure等函数式开发语言)底层都有ForkJoinPool的身影,在Java 1.8中引入的Parallel Stream其底层的并行计算也是由ForkJoinPool来完成的。
“分而治之”(divide and conquer)是Fork/Join框架的核心思想,Forks通过递归的形式将任务拆分成较小的独立的子任务,直到它足够简单以至于可以在一个异步任务中完成为止;Join则通过递归的方式将所有子任务的若干结果合并成一个结果,或者在子任务不关心结果是否返回的情况下,Join将等待所有的子任务完成各自的异步任务后“合并计算结果”,然后逐层向上汇总,直到将最终结果返回给执行线程。
ForkJoinPool是Fork/Join Framework在Java中的实现,同时它也是该框架最核心的类之一,ForkJoinPool是ExecutorService的一个具体实现,用于管理工作线程并为我们提供工具以及获取有关线程池状态和性能的信息等。ForkJoinTask是在ForkJoinPool内部执行的任务的基本类型,在ForkJoinPool中运行着的任务无论是RecursiveTask还是RecursiveAction都是ForkJoinTask的子类,前者在子任务运行结束后会返回计算结果,后者则不会有任何返回,而只是专注于子任务的运行本身
public abstract class ForkJoinTask<V> implements Future<V>, Serializable
2 ForkJoinPool中的任务
在ForkJoinPool中运行着的任务无论是RecursiveTask还是RecursiveAction都是ForkJoinTask的子类,前者在子任务运行结束后会返回计算结果,后者则不会有任何返回
2.1 RecursiveTask
public abstract class RecursiveTask<V> extends ForkJoinTask<V>{
protected abstract V compute();
}
- 继承自ForkJoinTask
- 还是个抽象类,也就是说我们需要自己提供实现,实现compute抽象方法
代码示例
- 实现RecursiveTask
public class RecursiveTaskSum extends RecursiveTask<Long> {
private final long[] numbers;
private final int startIndex;
private final int endIndex;
// 每个子任务运算的最多元素数量
private static final long THRESHOLD = 10_000L;
public RecursiveTaskSum(long[] numbers) {
this(numbers, 0, numbers.length);
}
private RecursiveTaskSum(long[] numbers, int startIndex, int endIndex) {
this.numbers = numbers;
this.startIndex = startIndex;
this.endIndex = endIndex;
}
@Override
protected Long compute() {
// 元素数量
int len = this.endIndex - this.startIndex;
// 当元素数量少于等于 THRESHOLD时,任务将不必再拆分
if (len <= THRESHOLD) {
// 直接计算
long sum = 0;
for (int i = 0; i < this.endIndex; i++) {
sum += this.numbers[i];
}
return sum;
}
// 当元素数量大于 THRESHOLD时,任务将拆分
// 拆分任务(一分为二,被拆分后的任务有可能还会被拆分:递归)
int tempEndIndex = this.startIndex + len / 2;
// 第一个子任务
RecursiveTaskSum firstTask =
new RecursiveTaskSum(numbers, startIndex, tempEndIndex);
// 异步执行第一个被拆分的子任务(子任务有可能还会被拆,这将取决于元素数量)
firstTask.fork();
RecursiveTaskSum secondTask =
new RecursiveTaskSum(numbers, tempEndIndex, endIndex);
// 异步执行第二个被拆分的子任务(子任务有可能还会被拆,这将取决于元素数量)
secondTask.fork();
// join等待子任务的运算结果
Long secondTaskResult = secondTask.join();
Long firstTaskResult = firstTask.join();
// 将子任务的结果相加然后返回
return (secondTaskResult + firstTaskResult);
}
}
- 测试
public static void main(String[] args) {
// 创建一个数组
long[] numbers = LongStream
.rangeClosed(1, 9_000_000).toArray();
// 定义RecursiveTask
RecursiveTaskSum forkJoinSum = new RecursiveTaskSum(numbers);
// 创建ForkJoinPool并提交执行RecursiveTask
Long sum = ForkJoinPool.commonPool().invoke(forkJoinSum);
// 输出结果
System.out.println(sum);
// validation result验证结果的正确性
assert sum == LongStream.rangeClosed(1, 9_000_000).sum();
}
这段代码就是对数组元素求和,会按照数组元素的个数进行拆分任务
重点在于在compute方法中如何进行任务的拆分。ForkJoinPool在运算的过程中首先会以递归的方式将任务拆分成2个子任务,子任务还会继续拆分,直到每一个子任务处理的数据量是10000个为止,然后在不同的线程中直接计算,最后将所有子任务的计算结果进行join并返回。
2.2 RecursiveAction
RecursiveAction类型的任务与RecursiveTask比较类似,只不过它更关注于子任务是否运行结束
public abstract class RecursiveAction extends ForkJoinTask<Void>
public class RecursiveActionExample extends RecursiveAction{
private List<Integer> numbers;
// 每个任务最多进行10个元素的计算
private static final int THRESHOLD = 10;
private int start;
private int end;
private int factor;
public RecursiveActionExample(List<Integer> numbers, int start,
int end, int factor) {
this.numbers = numbers;
this.start = start;
this.end = end;
this.factor = factor;
}
@Override
@Override
protected void compute(){
// 直接计算
if (end - start < THRESHOLD){
computeDirectly();
} else{
// 拆分
int middle = (end + start) / 2;
RecursiveActionExample taskOne =
new RecursiveActionExample(numbers, start, middle, factor);
RecursiveActionExample taskTwo =
new RecursiveActionExample(numbers, middle, end, factor);
this.invokeAll(taskOne, taskTwo);
}
}
private void computeDirectly() {
for (int i = start; i < end; i++) {
numbers.set(i, numbers.get(i) * factor);
}
}
public static void main(String[] args){
// 随机生成数字并且存入list中
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
list.add(current().nextInt(1_000));
}
// 输出原始数据
System.out.println(list);
// 定义 ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool();
// 定义RecursiveAction
RecursiveActionExample forkJoinTask =
new RecursiveActionExample(list, 0, 10, 10);
// 将forkJoinTask提交至ForkJoinPool
forkJoinPool.invoke(forkJoinTask);
System.out.println(list);
}
无论是RecursiveTask还是RecursiveAction,对任务的拆分与合并都是在compute方法中进行的,可见该方法的职责(fork,join,计算)太重,不够单一,且可测试性比较差,因此在Java 8版本中提供了接口Spliterator,其对任务的拆分有了进一步的高度抽象