这是我参与8月更文挑战的第16天,活动详情查看:8月更文挑战
ForkJoin 模式先把一个大任务分解(Fork)成许多个独立的子任务,然后起多线程并行去处理这些子任务。有可能子任务还是很大, 还需要进一步拆解, 最终得到足够小的任务。
ForkJoin
模式借助了现代计算机多核的优势并行去处理数据。
通常情况下,ForkJoin
模式将分解Fork
出来的子任务放入双端队列中, 然后几个启动线程从双端队列中获取任务并执行。子任务执行的结果放到一个队列里, 另起线程从队列中获取数据, 然后再进行局部结果的合并Join
得到了最终结果。
ForkJoin 框架
JUC
包提供了一套 ForkJoin
框架的实现,具体以 ForkJoinPool
线程池的形式提供,并且该线程池在 Java 8 的 Lambda 并行流实现中充当着底层框架的角色。
JUC 包的 ForkJoin
框架包含了如下组件:
-
ForkJoinPool: 执行任务的线程池,继承了
AbstractExecutorService
类。 -
ForkJoinWorkerThread: 执行任务的工作线程(即 ForkJoinPool 线程池里的线程)。 每个线程都维护着一个内部队列,用于存放“内部任务”。继承了 Thread 类。
-
ForkJoinTask: 一个用于 ForkJoinPool 的任务抽象类,实现了 Future 接口。
3.1 RecursiveTask: 带返回结果的递归执行任务,是 ForkJoinTask 的子类,在子任务带返回结果时使用。
3.2 RecursiveAction: 不返回结果的递归执行任务,是 ForkJoinTask 的子类,在子任务不 带返回结果时使用。
日常使用时,我们不会直接实现ForkJoinTask接口,而是直接按照自己的需求使用带返回值的 RecursiveTask, 或者不带返回值的 RecursiveAction 接口。实现compute()
方法,方法体中具体对任务进行执行或者拆分,拆分的过程很像很像归并排序的套路
。一般的伪代码如下:
if
任务足够小 直接返回结果
else
分割成 N 个子任务
依次调用每个子任务的 fork 方法执行子任务
依次调用每个子任务的 join 方法,等待子任务的完成,然后合并执行结果
案例实战
实现从0累加到1000000
定义任务实现
- 提前定义好了任务的规模,即,
THRESHOLD = 100000
如果累加的区间小于该阈值,那么直接进行计算,否则要对任务进行一分为二进行拆分。
public class AccumulateTask extends RecursiveTask<Long> {
private final long start;
private final long end;
private static final long THRESHOLD = 100000;
public AccumulateTask(long start, long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
long sum = 0;
if (end - start <= THRESHOLD) {
for (long i = start; i <= end; i++) {
sum += i;
}
System.out.println("task-"+Thread.currentThread().getName()+"-执行任务,计算" + start + "到" + end + "的和,结果是:" + sum);
} else {
// 任务过大,需要切割,Recursive 递归计算
System.out.println("task-"+Thread.currentThread().getName()+"-切割任务:将" + start + "到" + end + "的和一分为二");
long middle = (start + end) / 2;
// 切割成两个子任务
AccumulateTask lTask = new AccumulateTask(start, middle);
AccumulateTask rTask = new AccumulateTask(middle + 1, end);
// 依次调用每个子任务的 fork 方法执行子任务
lTask.fork();
rTask.fork();
// 等待子任务的完成,依次调用每个子任务的 join 方法合并执行结果
long leftResult = lTask.join();
long rightResult = rTask.join();
// 合并子任务执行结果
sum = leftResult + rightResult;
}
return sum;
}
}
- 任务提交
@Test
public void test() throws ExecutionException, InterruptedException {
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> task = forkJoinPool.submit(new AccumulateTask(0, 1000000));
System.out.println(task.get());
}
- 过程&结果输出
task-ForkJoinPool-1-worker-1-切割任务:将0到1000000的和一分为二
task-ForkJoinPool-1-worker-2-切割任务:将0到500000的和一分为二
task-ForkJoinPool-1-worker-3-切割任务:将500001到1000000的和一分为二
task-ForkJoinPool-1-worker-1-切割任务:将0到250000的和一分为二
task-ForkJoinPool-1-worker-0-切割任务:将250001到500000的和一分为二
task-ForkJoinPool-1-worker-3-切割任务:将500001到750000的和一分为二
task-ForkJoinPool-1-worker-0-切割任务:将250001到375000的和一分为二
task-ForkJoinPool-1-worker-2-切割任务:将125001到250000的和一分为二
task-ForkJoinPool-1-worker-1-切割任务:将0到125000的和一分为二
task-ForkJoinPool-1-worker-3-切割任务:将500001到625000的和一分为二
task-ForkJoinPool-1-worker-0-执行任务,计算250001到312500的和,结果是:17578156250
task-ForkJoinPool-1-worker-3-执行任务,计算500001到562500的和,结果是:33203156250
task-ForkJoinPool-1-worker-2-执行任务,计算125001到187500的和,结果是:9765656250
task-ForkJoinPool-1-worker-1-执行任务,计算0到62500的和,结果是:1953156250
task-ForkJoinPool-1-worker-0-执行任务,计算312501到375000的和,结果是:21484406250
task-ForkJoinPool-1-worker-1-执行任务,计算62501到125000的和,结果是:5859406250
task-ForkJoinPool-1-worker-2-执行任务,计算187501到250000的和,结果是:13671906250
task-ForkJoinPool-1-worker-0-切割任务:将375001到500000的和一分为二
task-ForkJoinPool-1-worker-3-执行任务,计算562501到625000的和,结果是:37109406250
task-ForkJoinPool-1-worker-3-切割任务:将625001到750000的和一分为二
task-ForkJoinPool-1-worker-2-执行任务,计算437501到500000的和,结果是:29296906250
task-ForkJoinPool-1-worker-0-执行任务,计算375001到437500的和,结果是:25390656250
task-ForkJoinPool-1-worker-3-执行任务,计算625001到687500的和,结果是:41015656250
task-ForkJoinPool-1-worker-0-切割任务:将750001到1000000的和一分为二
task-ForkJoinPool-1-worker-0-切割任务:将750001到875000的和一分为二
task-ForkJoinPool-1-worker-2-切割任务:将875001到1000000的和一分为二
task-ForkJoinPool-1-worker-3-执行任务,计算687501到750000的和,结果是:44921906250
task-ForkJoinPool-1-worker-2-执行任务,计算875001到937500的和,结果是:56640656250
task-ForkJoinPool-1-worker-2-执行任务,计算937501到1000000的和,结果是:60546906250
task-ForkJoinPool-1-worker-0-执行任务,计算750001到812500的和,结果是:48828156250
task-ForkJoinPool-1-worker-3-执行任务,计算812501到875000的和,结果是:52734406250
500000500000
ForkJoin核心构造参数
public ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory,
UncaughtExceptionHandler handler,
boolean asyncMode) {
this(checkParallelism(parallelism),
checkFactory(factory),
handler,
asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
"ForkJoinPool-" + nextPoolId() + "-worker-");
checkPermission();
}
-
parallelism: 可并行级别
ForkJoin
框架将依据parallelism
设定的级别,决定框架内并行执行的线程数量。并行的每一个任务都会有一个线程进行处理,但parallelism
属性并不是ForkJoin
框架中最大的线程数量;该属性也和ThreadPoolExecutor
线程池中的corePoolSize
、maximumPoolSize
属性有区别,因为ForkJoinPool
的结构和工作方式与ThreadPoolExecutor
完全不一样。ForkJoin
框架中可存在的线程数量和parallelism
参数值并不是绝对的关联. -
factory: 线程创建工厂
2.1 当
ForkJoin
框架创建一个新的线程时,同样会用到线程创建工厂。只不过这个线程工厂不再需要实现ThreadFactory
接口,而是需要实现ForkJoinWorkerThreadFactory
接口。后者是一个函数式接口,只需要实现一个名叫newThread
的方法。2.2 在
ForkJoin
框架中有一个默认的ForkJoinWorkerThreadFactory
接口实现DefaultForkJoinWorkerThreadFactory
-
handler: 异常捕获处理器
当执行的任务中出现异常,并从任务中被抛出时,就会被 handler 捕获。
-
asyncMode: 异步模式
4.1
asyncMode
参数表示任务是否为异步模式
,其默认值为false
。4.2 如果 asyncMode 为 true,表示子任务的执行遵循
FIFO
(先进先出)顺序,并且子任务不能被合并(join)
;4.3 如果 asyncMode 为 false,表示子任务的执行遵循
LIFO
(后进先出)顺序,并且子任务可以被合并(join)
。
Forkjoin默认构造
public ForkJoinPool() {
static final int MAX_CAP = 0x7fff; // max #workers - 1
this(Math.min(MAX_CAP, Runtime.getRuntime().availableProcessors()),
defaultForkJoinWorkerThreadFactory, null, false);
}
该构造函数的 parallelism 值为 CPU 核数;factory 值为 defaultForkJoinWorkerThreadFactory 默认的线程工厂;异常捕获处理器 handler 值为 null,表示不进行异常处理;异步模式 asyncMode 值为 false,使用 LIFO (后进先出)的、可以合并子任务的模式。