Fork/Join线程池是Java 7中引入的一个用于并行执行任务的框架,它的设计目的是充分利用多核处理器的计算能力,加快处理速度,提高性能。Fork/Join框架主要用于任务需要分解为多个子任务执行的场景,是一种分而治之的并行计算模型。它的核心思想是将一个大任务分解(Fork)成若干个小任务,如果这些小任务还太大,则继续分解,直到足够小可以直接计算,然后执行这些任务,并将结果合并(Join)。
核心组件
ForkJoinPool
ForkJoinPool是Fork/Join框架的线程池实现,它负责执行ForkJoinTask任务。ForkJoinPool通过使用工作窃取(work-stealing)算法来优化任务的执行,即空闲的线程可以从其他线程的工作队列中偷取任务来执行,以此来减少线程间的竞争和提高效率。
ForkJoinTask
ForkJoinTask是执行在ForkJoinPool中的任务的基类,有两个重要的子类:RecursiveAction和RecursiveTask。RecursiveAction代表没有返回结果的任务,而RecursiveTask代表有返回结果的任务。开发者通常需要继承这两个类之一来实现自己的并行任务。
工作原理
- 任务分解(Fork):将大任务分解为更小的任务,直到这些任务足够小,可以顺利执行而不需要进一步分解。
- 执行任务:并行执行这些任务。ForkJoinPool中的每个线程都维护自己的双端队列,用于存储待执行的任务。线程执行完队列中的一个任务后,会尝试执行下一个任务。
- 任务合并(Join):等待子任务完成,并合并其结果。
特点
- 工作窃取算法:Fork/Join框架通过工作窃取算法来减少线程之间的竞争,提高了线程的执行效率。当一个线程的任务队列为空时,它可以从其他线程的任务队列的尾部"偷取"任务来执行。
- 递归任务分解:Fork/Join框架鼓励使用递归方法来分解和执行任务,这对于很多算法来说是自然且高效的方式。
- 适用性:Fork/Join框架适用于可以并行处理且具有递归特性的任务,特别是在处理大数据集、图形渲染、并行数组操作等场景下表现出色。
使用场景
- 大数据处理
- 图像处理与渲染
- 并行数组操作
- 大规模计算任务
示例代码
import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;
public class SumTask extends RecursiveTask<Long> {
private final long[] numbers;
private final int start;
private final int end;
public static final long THRESHOLD = 10_000;
public SumTask(long[] numbers, int start, int end) {
this.numbers = numbers;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
int length = end - start;
if (length <= THRESHOLD) {
// 直接计算
return add(numbers, start, end);
} else {
// 继续分解任务
int split = start + length / 2;
SumTask leftTask = new SumTask(numbers, start, split);
SumTask rightTask = new SumTask(numbers, split, end);
invokeAll(leftTask, rightTask);
// 合并结果
return leftTask.join() + rightTask.join();
}
}
private long add(long[] numbers, int start, int end) {
long sum = 0;
for (int i = start; i < end; i++) {
sum += numbers[i];
}
return sum;
}
public static void main(String[] args) {
long[] numbers = new long[20_000];
// 初始化数组...
ForkJoinTask<Long> task = new SumTask(numbers, 0, numbers.length);
Long result = new ForkJoinPool().invoke(task);
System.out.println("Sum: " + result);
}
}
这个例子中,SumTask
继承自RecursiveTask<Long>
,用于计算一个长整型数组的总和。如果任务处理的数组段足够小,它会直接计算;否则,它会将任务一分为二,递归处理。
invokeAll 方法
invokeAll
方法是 ForkJoinTask
类中的一个方法,它在 ForkJoinPool
中并行执行一组任务。当有多个子任务需要并行执行,并且希望当前任务等待这些子任务全部完成时,可以使用 invokeAll
方法。
工作原理
-
invokeAll
方法接受一个任务集合作为参数。 - 它会提交所有这些任务到
ForkJoinPool
以并行执行。 - 当前线程(即调用
invokeAll
方法的任务)会阻塞,直到所有提交的任务都执行完成。
使用场景
invokeAll
方法通常在任务需要被分解成几个独立的子任务并且这些子任务可以并行处理时使用。通过并行执行这些子任务,可以有效地利用多核处理器的计算资源,从而提高应用程序的性能。
示例
假设有一个大任务,需要被分解为两个子任务 leftTask
和 rightTask
。可以使用 invokeAll
方法来并行执行这两个子任务,并等待它们完成。
SumTask leftTask = new SumTask(numbers, start, split);
SumTask rightTask = new SumTask(numbers, split, end);
// 并行执行 leftTask 和 rightTask,并等待它们完成
invokeAll(leftTask, rightTask);
在这个示例中,invokeAll(leftTask, rightTask)
会同时执行 leftTask
和 rightTask
。当前任务(可能是一个更大的 ForkJoinTask
)会在这两个任务完成之前阻塞,确保在进行下一步操作之前,所有子任务都已完成。
注意点
- 使用
invokeAll
方法时,需要考虑到任务分解的粒度。如果任务分解得太细,可能会导致大量的线程创建和上下文切换,反而降低程序的性能。 -
invokeAll
是同步的,当前任务会等待所有子任务完成。如果需要异步执行任务,可以单独使用fork
方法提交任务,然后在必要的时候通过join
方法等待任务完成。
invoke() 方法
invoke()
方法是 ForkJoinPool
类中的一个方法,用于在当前线程中执行给定的 ForkJoinTask
,并等待其完成,返回任务的结果。它是一种同步执行方式,即调用 invoke()
的线程会阻塞,直到提交给 ForkJoinPool
的任务完成并返回结果。
工作原理
- 当调用
invoke()
方法并传入一个ForkJoinTask
(如RecursiveTask
或RecursiveAction
)时,ForkJoinPool
会安排这个任务执行。 -
invoke()
方法会立即开始执行任务,调用线程(即调用invoke()
的线程)会等待任务完成。 - 对于
RecursiveTask
类型的任务,invoke()
方法会返回任务计算的结果。对于RecursiveAction
类型的任务(没有返回值的任务),invoke()
方法在任务完成后返回null
。
使用场景
invoke()
方法适用于需要立即执行一个任务,并且希望当前线程等待任务完成的场景。这通常用在需要并行处理的大型任务上,其中大任务被分解为多个小任务,ForkJoinPool
负责管理这些任务的执行。
示例代码
假设有一个计算数组元素总和的 SumTask
(继承自 RecursiveTask<Long>
),可以使用 invoke()
方法来执行这个任务并获取结果:
// 创建一个包含随机数的数组
long[] numbers = new long[1000];
// 填充数组...
// 创建一个ForkJoinTask来计算数组的总和
ForkJoinTask<Long> task = new SumTask(numbers, 0, numbers.length);
// 创建ForkJoinPool并使用invoke方法执行任务
Long result = new ForkJoinPool().invoke(task);
// 输出结果
System.out.println("Total sum: " + result);
在这个示例中,new ForkJoinPool().invoke(task)
会在 ForkJoinPool
中执行 SumTask
任务,并等待任务完成。invoke()
方法返回 SumTask
的计算结果,即数组元素的总和。
注意点
- 使用
invoke()
方法会导致调用线程阻塞,直到任务完成。这意味着如果任务执行时间很长,调用线程也会被长时间阻塞。 -
ForkJoinPool
采用工作窃取算法来优化任务的执行,但是如果任务分解得不合理(太细或太粗),可能会影响性能。 - 通常情况下,应避免创建多个
ForkJoinPool
实例,因为每个ForkJoinPool
实例都会创建许多线程,过多的线程可能会导致系统资源紧张,降低性能。在大多数情况下,使用ForkJoinPool
的静态共享实例(通过ForkJoinPool.commonPool()
获取)是足够的。