Fork/Join是Java并发编程中的一个重要概念,它基于"分治"(divide and conquer)的思想,尝试将所有可用的处理器内核使用起来帮助加速并行处理。
Fork/Join模型包含三个基本步骤:分解(Fork)、解决(Compute)和合并(Join)。在实际使用过程中,这种 “分而治之”的方法意味着框架首先要 fork ,递归地将任务分解为较小的独立子任务,直到它们足够简单以便异步执行。然后,join 部分开始工作,将所有子任务的结果递归地连接成单个结果,或者在返回 void 的任务的情况下,程序只是等待每个子任务执行完毕。
为了提供有效的并行执行,Fork/Join 框架使用了一个名为 ForkJoinPool 的线程池,用于管理 ForkJoinWorkerThread 类型的工作线程
ForkJoinPool 是 Fork/Join 框架的核心,是 ExecutorService 的一个实现,用于管理工作线程,并提供了一些工具来帮助获取有关线程池状态和性能的信息。
工作线程一次只能执行一个任务。
ForkJoinPool 线程池并不会为每个子任务创建一个单独的线程,相反,池中的每个线程都有自己的双端队列用于存储任务 ( double-ended queue )( 或 deque,发音 deck )。这种架构使用了一种名为工作窃取( work-stealing )算法来平衡线程的工作负载。
要怎么解释“工作窃取算法 ”呢 ?工作窃取算法的核心思想是,每个线程都维护一个双端队列,用于存储待执行的工作。当一个线程完成自己的工作后,它可以从其他线程的队列中窃取工作来执行。
简单来说,就是 空闲的线程试图从繁忙线程的 deques 中窃取工作。
默认情况下,每个工作线程从其自己的双端队列中获取任务。但如果自己的双端队列中的任务已经执行完毕,双端队列为空时,工作线程就会从另一个忙线程的双端队列尾部或全局入口队列中获取任务,因为这是最大概率可能找到工作的地方。
这种方法最大限度地减少了线程竞争任务的可能性。它还减少了工作线程寻找任务的次数,因为它首先在最大可用的工作块上工作。
在Java 8 中,创建 ForkJoinPool 实例的最简单的方式就是使用其静态方法 commonPool()。commonPool() 静态方法,见名思义,就是提供了对公共池的引用,公共池是每个 ForkJoinTask 的默认线程池。
根据Oracle 的官方文档,使用预定义的公共池可以减少资源消耗,因为它会阻止每个任务创建一个单独的线程池。
ForkJoinPool commonPool = ForkJoinPool.commonPool();
当然,你也可以使用构造方法直接创建,只需要指定线程池的并行级别,也就是要使用多少个核心线程。
int parallelism = 4; // 指定并行级别为4
ForkJoinPool forkJoinPool = new ForkJoinPool(parallelism);
ForkJoinTask 是 ForkJoinPool 线程之中执行的任务的基本类型。我们日常使用时,一般不直接使用 ForkJoinTask ,而是扩展它的两个子类中的任意一个:它有两个子类:RecursiveAction和RecursiveTask。RecursiveAction用于表示没有返回结果的任务,而RecursiveTask用于表示有返回结果的任务。这两个子类都需要实现compute()方法来定义任务的执行逻辑。
在这里仅为了演示,示例尽可能的简单些,因此,我们的示例,执行了一个比较荒谬的任务:将输入转为大写并记录。
所有的代码如下所示:
public class CustomRecursiveAction extends RecursiveAction {
private String workload = "";
private static final int THRESHOLD = 4; // 核心线程数
private static Logger logger =
Logger.getAnonymousLogger();
public CustomRecursiveAction(String workload) {
this.workload = workload;
}
@Override
protected void compute() {
if (workload.length() > THRESHOLD) {
// 任务拆分
ForkJoinTask.invokeAll(createSubtasks());
} else {
// 执行业务逻辑
processing(workload);
}
}
private List<CustomRecursiveAction> createSubtasks() {
List<CustomRecursiveAction> subtasks = new ArrayList<>();
String partOne = workload.substring(0, workload.length() / 2);
String partTwo = workload.substring(workload.length() / 2, workload.length());
subtasks.add(new CustomRecursiveAction(partOne));
subtasks.add(new CustomRecursiveAction(partTwo));
return subtasks;
}
private void processing(String work) {
String result = work.toUpperCase();
logger.info("This result - (" + result + ") - was processed by "
+ Thread.currentThread().getName());
}
在这个示例中,我们使用了一个字符串类型 ( String ) 的名为 workload 属性来表示要处理的工作单元。同时,为了演示 Fork/Join 框架的 fork 行为,在该示例中,如果 workload.length() 大于指定的阈值,那么就使用 createSubtask() 方法拆分任务。
在createSubtasks() 方法中,输入的字符串被递归地划分为子串,然后创建基于这些子串的 CustomRecursiveTask 实例。
当递归分割字符串完毕时,createSubtasks() 方法返回 List 作为结果。然后在compute() 方法中使用 invokeAll() 方法将任务列表提交给 ForkJoinPool 线程池。
对于有返回值的任务,除了将每个子任务的结果在一个结果中合并,其它逻辑和 RecursiveAction 都差不多。
public class CustomRecursiveTask extends RecursiveTask<Integer> {
private int[] arr;
private static final int THRESHOLD = 20;
public CustomRecursiveTask(int[] arr) {
this.arr = arr;
}
@Override
protected Integer compute() {
if (arr.length > THRESHOLD) {
// 任务拆分
return ForkJoinTask.invokeAll(createSubtasks())
.stream()
.mapToInt(ForkJoinTask::join)
.sum();
} else {
// 执行业务逻辑
return processing(arr);
}
}
private Collection<CustomRecursiveTask> createSubtasks() {
List<CustomRecursiveTask> dividedTasks = new ArrayList<>();
dividedTasks.add(new CustomRecursiveTask(
Arrays.copyOfRange(arr, 0, arr.length / 2)));
dividedTasks.add(new CustomRecursiveTask(
Arrays.copyOfRange(arr, arr.length / 2, arr.length)));
return dividedTasks;
}
private Integer processing(int[] arr) {
return Arrays.stream(arr)
.filter(a -> a > 10 && a < 27)
.map(a -> a * 10)
.sum();
}
在上面这个示例中,任务由存储在 CustomRecursiveTask 类的 arr 字段中的数组表示。createSubtask() 方法递归地将任务划分为较小的工作,直到每个部分小于阈值。
然后,invokeAll()方法将子任务提交给公共拉取并返回 Future 列表。
要触发执行,需要为每个子任务调用 join() 方法。
上面这个示例中,我们使用了 Java 8 的流 ( Stream ) API , sum() 方法用于将子结果组合到最终结果中。
只要使用很少的方法,就可以把任务提交到 ForkJoinPool 线程池中。
这两个方法的调用方式是相同的
forkJoinPool.execute(customRecursiveTask);
int result = customRecursiveTask.join();
int result = forkJoinPool.invoke(customRecursiveTask);
将ForkJoinTasks序列提交给ForkJoinPool的最方便的方法它将任务作为参数(两个任务,varargs或集合),fork它们,并按照生成它们的顺序返回Future对象的集合。
fork() 方法将任务提交给线程池,但不会触发任务的执行;join() 方法则用于触发任务的执行。在 RecursiveAction 的情况下,join() 返回 null,但对于 RecursiveTask ,它返回任务执行的结果。
customRecursiveTaskFirst.fork();
result = customRecursiveTaskLast.join();
上面的RecursiveTask 示例中,我们使用 invokeAll() 方法向线程池提交一系列子任务。同样的工作,也可以使用 fork() 和 join() 来完成,但这可能会对结果的排序产生影响。
为了避免混淆,当涉及到多个任务且要保证任务的顺序时,通常都是使用 ForkJoinPool.invokeAll()。
尽管 Fork/Join 框架可以加速处理大型任务,但是我们在使用的时候还是应该遵循一些原则:使用尽可能少的线程池,设置合理的阈值,合理拆分子任务,避免在 ForkJoingTasks 中出现任何阻塞。