ForkJoinPool 是Java 1.7 引入的一种新的并发框架—— ForkJoin Framework。
ForkJoinPool 主要用于实现“分而治之”的算法,特别是分治之后递归调用的函数,例如 quick sort 等。
ForkJoinPool 最适合的是计算密集型的任务,如果存在 I/O,线程间同步,sleep() 等会造成线程长时间阻塞的情况时,最好配合使用 ManagedBlocker。
ForkJoinPool & ForkJoinTask 概述:
-
ForkJoinTask:我们要使用 ForkJoin 框架,必须首先创建一个 ForkJoin 任务。它提供在任务中执行 fork() 和 join() 操作的机制,通常情况下我们不需要直接继承 ForkJoinTask 类,而只需要继承它的子类,ForkJoin 框架提供了以下两个子类:
-
RecursiveAction:用于没有返回结果的任务。
它是一种没有任何返回值的任务。只是做一些工作,比如写数据到磁盘,然后就退出了。 一个RecursiveAction可以把自己的工作分割成更小的几块, 这样它们可以由独立的线程或者CPU执行。 我们可以通过继承来实现一个RecursiveAction。 -
RecursiveTask :用于有返回结果的任务。
它是一种会返回结果的任务。可以将自己的工作分割为若干更小任务,并将这些子任务的执行合并到一个集体结果。 可以有几个水平的分割和合并
-
-
ForkJoinPool :ForkJoinTask 需要通过 ForkJoinPool 来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。
package com.freeman;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.*;
import java.util.stream.LongStream;
public class ForkJoinPoolDemo {
public static void main(String[] args) {
test1();
test2();
}
public static void test1() {
try {
// 创建包含Runtime.getRuntime().availableProcessors()返回值作为个数的并行线程的ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool();
// 创建ForkJoin 任务
ForkJoinTask<Long> task = new ForkJoinSumCalculate(0L, 500000000L);
// 提交可分解的PrintTask任务
ForkJoinTask<Long> sum = forkJoinPool.submit(task);
// 关闭线程池
forkJoinPool.shutdown();
System.out.println("计算结果: "+sum.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
public static void test2() {
// ForkJoinPool 的每个工作线程都维护着一个工作队列(WorkQueue),这是一个双端队列(Deque),里面存放的对象是任务(ForkJoinTask)。
// 每个工作线程在运行中产生新的任务(通常是因为调用了 fork())时,会放入工作队列的队尾,并且工作线程在处理自己的工作队列时,使用的是 LIFO 方式,也就是说每次从队尾取出任务来执行。
// 每个工作线程在处理自己的工作队列同时,会尝试窃取一个任务(或是来自于刚刚提交到 pool 的任务,或是来自于其他工作线程的工作队列),窃取的任务位于其他线程的工作队列的队首,也就是说工作线程在窃取其他工作线程的任务时,使用的是 FIFO 方式。
// 在遇到 join() 时,如果需要 join 的任务尚未完成,则会先处理其他任务,并等待其完成。
// 在既没有自己的任务,也没有可以窃取的任务时,进入休眠。
ForkJoinPool pool = new ForkJoinPool(); // ForkJoinPool.commonPool()
// 创建ForkJoin 任务
ForkJoinTask<Long> task = new ForkJoinSumCalculate(0L, 500000000L);
// ForkJoinPool 执行任务
Long sum = pool.invoke(task);
System.out.println("计算结果: "+sum);
}
//java8 新特性
public static void testJava8(){
Instant start = Instant.now();
Long sum = LongStream.rangeClosed(0L, 500000000L).parallel().reduce(0L, Long::sum);
System.out.println("计算结果: "+sum);
Instant end = Instant.now();
System.out.println("耗费时间为:" + Duration.between(start, end).toMillis());
}
}
class ForkJoinSumCalculate extends RecursiveTask<Long>{
private static final long serialVersionUID = -1L;
private long start;
private long end;
private static final long THURSHOLD = 10000L; // 停止继续拆分的临界值
public ForkJoinSumCalculate(long start, long end) {
this.start = start;
this.end = end;
}
// compute 把任务拆分,拆到不能再拆,再计算
@Override
protected Long compute() {
long num = end - start;
if(num <= THURSHOLD){
long sum = 0L;
for (long i = start; i <= end; i++) {
sum += i;
}
return sum;
}else{
long middle = (start + end) / 2;
// 将大任务分解成两个小任务,同时压入线程队列
ForkJoinSumCalculate left = new ForkJoinSumCalculate(start, middle);
ForkJoinSumCalculate right = new ForkJoinSumCalculate(middle+1, end);
// ForkJoinTask#fork 方法只做一件事,既是把任务推入当前工作线程的工作队列里。
left.fork();
right.fork();
// ForkJoinTask join 方法
// 1. 检查调用 join() 的线程是否是 ForkJoinThread 线程。如果不是(例如 main 线程),则阻塞当前线程,等待任务完成。如果是,则不阻塞。
// 2. 查看任务的完成状态,如果已经完成,直接返回结果。
// 3. 如果任务尚未完成,但处于自己的工作队列内,则完成它。
// 4. 如果任务已经被其他的工作线程偷走,则窃取这个小偷的工作队列内的任务(以 FIFO 方式),执行,以期帮助它早日完成欲 join 的任务。
// 5. 如果偷走任务的小偷也已经把自己的任务全部做完,正在等待需要 join 的任务时,则找到小偷的小偷,帮助它完成它的任务。
// 6. 递归地执行第5步。
return left.join() + right.join();
}
}
}