本篇博客记录一下ForkJoinPool相关的内容,主要涉及基本的设计思想和使用方式。
一、前言
Fork/Join
ForkJoinPool继承自AbstractExecutorService,是JDK1.7引入的并行处理框架,
作为Fork/Join型线程池的实现。
ForkJoinPool的基本思想是:
将大任务分割(Fork)成多个小任务;
多个子任务可以被多个线程并发执行;
最后将子任务聚合(Join)起来作为大任务的结果。
盗图一张,ForkJoinPool执行任务的思想类似于,实际上有点分治递归的感觉:
为了支持这种能力,JDK 1.7中专门定义了对应的任务类型ForkJoinTask。
不过我们一般不需要直接使用ForkJoinTask,仅需要继承它的子类RecursiveAction或RecursiveTask,
并实现对应的抽象方法compute即可。
其中,RecursiveAction是不带返回值的Fork/Join型任务,使用此类任务时并不产生结果,也就不涉及到结果的合并;
而RecursiveTask是带返回值的Fork/Join型任务,使用此类任务需要我们进行结果的合并。
Work-Stealing
相比于ThreadPoolExecutor,ForkJoinPool并发处理子任务时,引入了Work-Stealing算法。
Work-Stealing的思想是:
线程池中的每个线程,都有一个与之关联的任务队列(双端队列)。
线程每次都优先从与自己关联队列的头部取出任务来运行。
如果某个线程执行完当前任务后,发现关联的队列已空,
就会尝试从其它线程关联队列的尾部“窃取”任务来执行。
Work-Stealing算法的优点是充分利用线程进行并行计算,一定程度上减少了线程间的竞争,
适用于不同任务耗时相差比较大的场景。
与ThreadPoolExecutor对比
ForkJoinPool和ThreadPoolExecutor都是线程池,它们之间的不同点在于:
ThreadPoolExecutor只能执行Runnable和Callable任务,
而ForkJoinPool不仅可以执行Runnable和Callable 任务,
还可以执行ForkJoinTask,从而满足并行地实现分治算法的需要。
ThreadPoolExecutor中,任务的执行顺序是FIFO的,
所以后面的任务需要等待前面任务开始执行后才能被取出;
而ForkJoinPool每个线程有自己的任务队列,并在此基础上实现了Work-Stealing的功能,
使得在某些情况下,ForkJoinPool能更大程度的提高并发效率。
不过,如果ForkJoinPool需要执行的任务耗时很平均,
由于窃取任务时不同线程需要抢占锁,有可能会造成额外的时间消耗。
此外,Work-Stealing算法中,每个线程都需要维护双端队列,
这也会造成较大的内存消耗。
因此整体来讲,ForkJoinPool和ThreadPoolExecutor各有千秋,
需结合具体的业务场景来使用。
二、构造函数
接下来,我们看看ForkJoinPool的构造函数:
//无参数时,ForkJoinPool的线程数上限取决于处理器的数量
//使用的线程工厂类型为DefaultForkJoinWorkerThreadFactory
public ForkJoinPool() {
this(Math.min(MAX_CAP, Runtime.getRuntime().availableProcessors()),
defaultForkJoinWorkerThreadFactory, null, false);
}
//可以通过参数设置线程数上限