文章目录
1.介绍
fork/join 框架是在 Java 7 中提出的。它提供了一些工具,通过尝试使用所有可用的处理器内核来帮助加速并行处理——这是通过分而治之的方法来实现的。
在实践中,这意味着框架首先“分叉”,递归地将任务分解为更小的独立子任务,直到它们简单到可以异步执行。
之后,“join”部分开始,其中所有子任务的结果递归地合并为一个结果,或者在任务返回void的情况下,程序只是等待直到每个子任务都执行完毕。
为了提供有效的并行执行,fork/join 框架使用称为 ForkJoinPool 的线程池,它管理 ForkJoinWorkerThread 类型的工作线程。
2. ForkJoinPool
ForkJoinPool 是框架的核心。 它是 ExecutorService 的一个实现,它管理工作线程并为我们提供工具来获取有关线程池状态和性能的信息。
工作线程一次只能执行一个任务,但 ForkJoinPool 不会为每个子任务创建一个单独的线程。 相反,池中的每个线程都有自己的双端队列(或双端队列,发音为deck)来存储任务。
这种架构对于在工作窃取算法的帮助下平衡线程的工作负载至关重要。
2.1. 工作窃取算法
所谓工作窃取算法,是指一个Worker线程在执行完毕自己队列中的任务之后,可以窃取其他线程队列中的任务来执行,从而实现负载均衡,以防有的线程很空闲,有的线程很忙。
简单地说——空闲线程试图从繁忙线程的双端队列中“窃取”工作。
默认情况下,工作线程从它自己的双端队列的头部获取任务。 当它为空时,线程从另一个繁忙线程的双端队列的尾部或全局入口队列中获取任务,因为这是最大的工作部分可能所在的位置。
这种方法最大限度地减少了线程竞争任务的可能性。 它还减少了线程必须去寻找工作的次数,因为它首先处理最大的可用工作块。
2.2. ForkJoinPool 实例化
在 Java 8 中,访问 ForkJoinPool 实例的最便捷方法是使用其静态方法 commonPool()。 顾名思义,这将提供对公共池的引用,这是每个 ForkJoinTask 的默认线程池。
根据 Oracle 的文档,使用预定义的公共池可以减少资源消耗,因为这会阻止为每个任务创建单独的线程池。
ForkJoinPool commonPool = ForkJoinPool.commonPool();
通过创建 ForkJoinPool 并将其分配给实用程序类的公共静态字段,可以在 Java 7 中实现相同的行为:
public static ForkJoinPool forkJoinPool = new ForkJoinPool(2);
使用 ForkJoinPool 的构造函数,可以创建具有特定并行度、线程工厂和异常处理程序级别的自定义线程池。 在上面的示例中,池的并行度级别为 2。这意味着池将使用 2 个处理器内核。
3. ForkJoinTask
ForkJoinTask 是在 ForkJoinPool 中执行的任务的基本类型。 在实践中,它的两个子类之一应该被扩展:用于空任务的 RecursiveAction 和用于返回值的任务的 RecursiveTask。 它们都有一个抽象方法计算(),其中定义了任务的逻辑。
3.1. RecursiveAction 例子
在下面的示例中,要处理的工作单元由称为工作负载的字符串表示。 出于演示目的,该任务是一个无意义的任务:它只是将其输入大写并记录下来。
为了演示框架的分叉行为,该示例使用 createSubtask() 方法在工作负载.length() 大于指定阈值时拆分任务。
字符串被递归地划分为子字符串,创建基于这些子字符串的 CustomRecursiveTask 实例。
因此,该方法返回一个 List。
该列表使用 invokeAll() 方法提交给 ForkJoinPool:
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);
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());
}
}
此模式可用于开发自己的 RecursiveAction 类。 为此,创建一个代表工作总量的对象,选择合适的阈值,定义划分工作的方法,并定义完成工作的方法。
3.2. RecursiveTask
对于有返回值的任务,这里的逻辑是类似的,只是每个子任务的结果统一在一个结果中:
public class CustomRecursiveTask extends RecursiveTask<Integer> {
private int[] arr;
private static final int THRESHOLD = 5;
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();
}
public static void main(String[] args) {
ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
Integer invoke = forkJoinPool.invoke(new CustomRecursiveTask(new int[]{11, 21, 11, 12, 13, 14, 15, 16, 17}));
System.out.println(invoke);
}
}
在此示例中,工作由存储在 CustomRecursiveTask 类的 arr 字段中的数组表示。 createSubtasks() 方法递归地将任务划分为更小的工作块,直到每个工作块都小于阈值。 然后,invokeAll() 方法将子任务提交到公共池并返回一个 Future 列表。
为了触发执行,会为每个子任务调用 join() 方法。
在这个例子中,这是使用 Java 8 的 Stream API 完成的; sum() 方法用作将子结果组合成最终结果的表示。
4.向 ForkJoinPool 提交任务
要将任务提交到线程池,可以使用的方法很少。
submit() 或 execute() 方法(它们的用例是相同的):
forkJoinPool.execute(customRecursiveTask);
int result = customRecursiveTask.join();
invoke() 方法 fork 任务并等待结果,不需要任何手动加入:
int result = forkJoinPool.invoke(customRecursiveTask);
invokeAll() 方法是将 ForkJoinTasks 序列提交到 ForkJoinPool 的最方便的方法。 它将任务作为参数(两个任务、var args 或一个集合),然后 fork 按照它们生成的顺序返回 Future 对象的集合。
或者,以使用单独的 fork() 和 join() 方法。 fork() 方法向池提交任务,但不会触发其执行。 join() 方法必须用于此目的。 在 RecursiveAction 的情况下,join() 只返回 null; 对于 RecursiveTask,它返回任务执行的结果:
customRecursiveTaskFirst.fork();
result = customRecursiveTaskLast.join();
在RecursiveTask 示例中,使用 invokeAll() 方法将一系列子任务提交到池中。 可以使用 fork() 和 join() 完成相同的工作,尽管这会对结果的排序产生影响。
为避免混淆,通常最好使用 invokeAll() 方法向 ForkJoinPool 提交多个任务。
5.总结
使用 fork/join 框架可以加快大型任务的处理速度,但要实现此结果,应遵循一些准则:
- 使用尽可能少的线程池——在大多数情况下,最好的决定是为每个应用程序或系统使用一个线程池
- 使用默认的公共线程池,如果不需要特殊调优
- 使用合理的阈值将 ForkJoinTask 拆分为子任务
- 避免 ForkJoinTasks 中的任何阻塞
任务执行方法总结
非fork/join客户端调用 | 从 fork/join 计算中调用 | |
---|---|---|
异步执行 | execute(ForkJoinTask) | ForkJoinTask.fork |
等待和获取结果 | invoke(ForkJoinTask) | ForkJoinTask.invoke |
执行获取Future | submit(ForkJoinTask) | ForkJoinTask.fork (ForkJoinTasks are Futures) |