Fork/Join 框架
Java 的 Fork/Join 框架是 JDK 7 中引入的一种并行处理框架,基于分而治之的思想,专为在多核处理器上执行任务的并行分治而设计。它通过递归分解任务、并发执行这些子任务并最终合并结果来提高性能。这个框架非常适合执行那些可以递归拆分成多个小任务的计算密集型操作。核心类是 ForkJoinPool
和 ForkJoinTask
。
核心概念
- Fork/Join 机制:
- Fork:将任务拆分成多个子任务,每个子任务可以再次被拆分,直到任务足够简单,适合直接执行。
- Join:当子任务完成时,结果会被汇总,返回最终结果。
- 分治算法:
- Fork/Join 框架基于分治算法的思想。任务被不断拆分(fork),直到达到一个临界点(通常是任务足够小),然后每个子任务并行执行。执行完的结果通过 join 操作汇总。
核心类
Fork/Join 计算框架主要由两部分组成:分治任务的线程池 ForkJoinPool 和分治任务 ForkJoinTask。类似ThreadPoolExecutor和Runnable之间的关系。
-
ForkJoinPool
:这是 Fork/Join 框架的核心线程池,负责管理工作线程和任务调度。它的工作原理类似于ThreadPoolExecutor
,但它特别设计用于分治任务,使用工作窃取(work-stealing)算法来优化任务调度。工作窃取算法(Work-Stealing Algorithm):
- 每个工作线程维护一个双端队列。工作线程优先从自己队列的头部获取任务并执行。当某个线程没有任务可执行时,它会从其他线程队列的尾部窃取任务。这种机制确保了 CPU 的高利用率,减少了线程间的等待时间。
-
ForkJoinTask
这是任务的基类,有两个重要的子类:
RecursiveTask<V>
:适用于有返回值的任务。RecursiveAction
:适用于没有返回值的任务。
使用示例
public class Fibonacci extends RecursiveTask<Integer> {
private final int n;
public Fibonacci(int n) {
this.n = n;
}
@Override
protected Integer compute() {
if (n<=1){
return n;
}
Fibonacci f1=new Fibonacci(n-1);
Fibonacci f2=new Fibonacci(n-2);
// 拆分成子任务
f1.fork();
return f2.compute()+f1.join();//等待f1执行完并获取结果
}
public static void main(String[] args) {
Fibonacci task=new Fibonacci(10);
ForkJoinPool pool=new ForkJoinPool();
Integer result = pool.invoke(task);
System.out.println("结果是"+result);
}
}
输出:
结果是55
通过 f1.fork()
将 f1
(计算 F(n-1)
的任务)提交给 ForkJoinPool
以并行执行。当前线程则直接计算 f2.compute()
(即 F(n-2)
的值),并调用 f1.join()
等待并获取 F(n-1)
的计算结果。
主函数创建一个 ForkJoinPool 线程池以及一个用于计算斐波那契数列的 Fibonacci 分治任务。然后,通过调用 ForkJoinPool 线程池的 invoke()
方法来启动分治任务。
ForkJoinPool 的工作原理。
任务提交:
- 任务可以通过 ForkJoinPool 的
invoke()
或submit()
方法提交。当一个任务被提交到 ForkJoinPool 后,池会将其分配到内部的任务队列中。每个线程都有自己的任务队列,用来存储它要执行的任务。
任务拆分与执行:
- 在任务的
compute()
方法中,任务会根据业务逻辑被拆分成多个子任务。子任务会被分配到当前线程的任务队列中等待执行。Fork/Join 框架支持递归的任务拆分,使得大任务能够不断被细化,直到变成足够小的子任务。
任务窃取机制:
- 核心机制:为了避免线程在没有任务时空闲,ForkJoinPool 引入了“任务窃取”机制。当一个工作线程的任务队列为空时,它会从其他线程的任务队列中窃取任务执行。这样做的目的是尽可能地平衡负载,提高 CPU 使用率。
- 双端队列:ForkJoinPool 的任务队列是一个双端队列(deque)。工作线程会从队列的头部取任务执行,而空闲线程会从其他线程任务队列的尾部窃取任务执行。这种双端设计可以减少任务窃取带来的冲突,降低线程之间的竞争。
递归与合并:
- 当一个任务被分解成多个子任务时,ForkJoinPool 可以并行执行这些子任务。通过
fork()
方法将子任务交给线程池,主线程则继续执行其他任务,直到通过join()
合并这些子任务的结果。最终,任务的计算结果将被组合成最终输出。
ForkJoinPool
和 ThreadPoolExecutor
是 Java 中两种重要的线程池实现,它们在设计理念、应用场景和任务处理机制上都有所不同。
ForkJoinPool 与 ThreadPoolExecutor 区别
1. 设计目标
- ForkJoinPool:专为并行任务设计,主要用于需要将大任务递归分解为多个子任务的场景,如分治算法、递归操作等。它的设计目标是通过任务的拆分和合并(Fork/Join),充分利用多核 CPU 的并行计算能力。
- ThreadPoolExecutor:用于处理一组独立、无关联的任务。适合场景包括执行多个并发的任务,但任务之间没有直接依赖,或者任务的生命周期比较独立,不涉及任务的拆分与合并。
2. 任务拆分与合并
- ForkJoinPool:支持任务递归分解。通过
fork()
将大任务拆分为多个子任务,通过join()
合并子任务的结果。这种分治模式非常适合需要递归计算的任务,如并行处理大量数据、递归算法等。 - ThreadPoolExecutor:不支持任务的递归分解,它的任务是一次性提交的,每个任务独立执行。任务提交后由线程池调度执行,执行完毕后线程返回等待下一个任务。
3. 工作窃取(Work Stealing)机制
- ForkJoinPool:采用了工作窃取机制(Work Stealing)。当某个线程完成了自己任务队列中的所有任务时,它可以从其他线程的任务队列中窃取任务执行。任务队列是双端队列,工作线程从队列头部获取任务,窃取线程从队列尾部窃取任务。这种设计提高了线程的利用率,减少了空闲线程的浪费。
- ThreadPoolExecutor:没有工作窃取机制。线程从共享任务队列中按顺序获取任务执行,任务队列是 FIFO(先进先出)的。
4. 任务类型
- ForkJoinPool:适合处理大规模并行任务,特别是递归任务。典型的任务是可以被拆解成更小的子任务,并行执行后再合并结果。
- ThreadPoolExecutor:适合处理相对独立的、互不依赖的任务。每个任务相对独立,不需要相互等待结果。典型场景如处理 HTTP 请求、执行异步任务等。
5. 任务队列
- ForkJoinPool:每个工作线程都有一个独立的任务队列,任务队列为双端队列。工作线程从队列头部获取任务,窃取线程从队列尾部获取任务。
- ThreadPoolExecutor:所有线程共享同一个任务队列,通常是阻塞队列(
BlockingQueue
)。任务是按顺序从队列中取出并执行的。
6. 执行效率
- ForkJoinPool:通过任务拆分、并行执行和工作窃取机制,能够更好地利用多核 CPU 的计算能力,因此在大规模并行计算场景下效率更高。
- ThreadPoolExecutor:任务没有拆分和合并的步骤,因此适合处理独立的任务。在处理独立任务时性能更好,但在需要递归分解任务的场景中表现不如 ForkJoinPool。