Fork/Join框架是Java7提供了的一个用于并行执行的任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
Fork 递归地将任务分解为较小的独立子任务,直到它们足够简单以便异步执行。
Join 将所有子任务的结果递归地连接成单个结果,或者在返回void的任务的情况下,程序只是等待每个子任务执行完毕。
比如计算 1+2+......+10000,可以分割成10个子任务,每个子任务对1000个数进行求和,最终汇总这10个子任务的结果。如下图所示:
Fork/Join的特性:
ForkJoinPool不是为了替代 ExecutorService,而是它的补充,在某些应用场景下性能比ExecutorService更好
ForkJoinPool主要用于实现“分而治之”的算法,特别是分治之后递归调用的函数,例如 quick sort 等;
ForkJoinPool 最适合的是计算密集型的任务。
ForkJoinPool的宗旨是使用少量的线程来处理大量的任务,而CPU密集型任务,当一个大任务分解成多个子任务后,多个线程获取到多个处理器的时间分片,可以并行的执行子任务。
Fork/Join框架其实就是指由ForkJoinPool作为线程池、ForkJoinTask为任务、ForkJoinWorkerThread作为执行任务的具体线程实体这三者构成的任务调度机制。
ForkJoinPool是fork/join框架的核心,是ExecutorService的一个实现,用于管理工作线程,并提供了一些工具来帮助获取有关线程池状态和性能的信息。工作线程一次只能执行一个任务。ForkJoinPool线程池中的每个线程都有自己的双端队列用于存储任务(double-ended queue 或 deque),这种架构使用了一种名为工作窃取(work-stealing)算法来平衡线程的工作负载。
2.1.1 工作窃取(work-stealing)算法
工作窃取(work-stealing)算法:空闲的线程试图从繁忙线程的deques中窃取工作
默认情况下,每个工作线程从其自己的双端队列中获取任务。但如果自己的双端队列中的任务已经执行完毕,双端队列为空,工作线程就会从另一个忙线程的双端队列尾部或全局入口队列中获取任务,因为这是最大概率可能找到工作的地方。这种方法最大下限度地减少了线程竞争任务地可能性。它还减少了工作线程寻找任务地次数,因为它首先在最大可用地工作块上工作。
通常会使用双端队列,被窃取任务线程永远从双端队列的尾部拿任务执行,而窃取任务的线程永远从双端队列的头部拿任务执行。
工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。
ForkJoinPool 的每个工作线程都维护着一个工作队列(WorkQueue),这是一个双端队列(Deque),里面存放的对象是任务(ForkJoinTask)。
每个工作线程在运行中产生新的任务(通常是因为调用了 fork())时,会放入工作队列的队尾,并且工作线程在处理自己的工作队列时,使用的是 LIFO(Last In, First Out) 方式,也就是说每次从队尾取出任务来执行。
每个工作线程在处理自己的工作队列同时,会尝试窃取一个任务,窃取的任务位于其他线程的工作队列的队首,也就是说工作线程在窃取其他工作线程的任务时,使用的是 FIFO(First In, First Out ) 方式。
在遇到 join() 时,如果需要 join 的任务尚未完成,则会先处理其他任务,直到目标的任务方法被告知已经结束(通过isDone()方法),所有的任务都是无阻塞的完成。
在既没有自己的任务,也没有可以窃取的任务时,进入休眠。
ForkJoinWorkerThread直接继承了Thread,但是仅仅是为了增加一些额外的功能,并没有对线程的调度执行做任何更改。ForkJoinWorkerThread是被ForkJoinPool管理的工作线程,由它来执行ForkJoinTask。ForkJoinWorkerThread的首要任务就是执行自己的这个双端任务队列中的任务,其次是窃取其他线程的工作队列。
ForkJoinWorkerThread 依附于ForkJoinPool而存活,如果ForkJoinPool的销毁了,它也会跟着结束。
ForkJoinTask 是 ForkJoinPool线程之中执行的任务的基本类型,它实现了Future接口。
ForkJoinTask :我们要使用 ForkJoin 框架,必须首先创建一个 ForkJoin 任务。它提供在任务中执行 fork() 和 join() 操作的机制,通常情况下我们不需要直接继承 ForkJoinTask 类,而只需要继承它的子类,Fork/Join框架提供类以下几个子类:
RecursiveAction:用于没有返回结果的任务。
RecursiveTask:用于有返回结果的任务。
RecursiveAction、RecursiveTask这两个类都有一个抽象方法compute() ,用于定义任务的逻辑。使用:就是继承任意一个类,然后实现 compute() 方法。
2.3.1 常用方法
1.fork()方法:把任务推入当前工作线程的工作队列里(安排任务异步执行)
2.join()方法:等待执行结果
join() 的工作比较复杂
检查调用 join() 的线程是否是 ForkJoinThread 线程。如果不是(例如 main 线程),则阻塞当前线程,等待任务完成。如果是,则不阻塞。
查看任务的完成状态,如果已经完成,直接返回结果。
如果任务尚未完成,但处于自己的工作队列内,则调用
doExec()
完成它。如果任务不再当前队列的尾部位置(已经被其他的工作线程偷走)调用
wt.pool.awaitJoin(w, this, 0L)
,窃取这个小偷的工作队列内的任务(以 FIFO 方式)执行,以期帮助它早日完成预 join 的任务。如果偷走任务的小偷(Thread)也已经把自己的任务全部做完,正在等待需要 Join 的任务时,则找到小偷的小偷,帮助它完成它的任务。
递归地执行第 5 步。
private int doJoin() { int s; Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w; //<0 已完成(包含任务取消、任务异常) return (s = status) < 0 ? s : ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ? (w = (wt = (ForkJoinWorkerThread)t).workQueue). tryUnpush(this) && (s = doExec()) < 0 ? s : wt.pool.awaitJoin(w, this, 0L) : externalAwaitDone(); }
3.invoke() 方法(立即执行任务,并等待返回结果)
4.invokeAll()方法(批量执行任务,并等待它们执行结束)当涉及到多个任务且要保证任务的顺序时,可使用invokeAll(),fork()与join()不保证顺序。
//最佳应用场景:多核、多内存、可以分割计算再合并的计算密集型任务
public class TaskDemo extends RecursiveTask {
private static final int THRESHOLD = 100;//阈值
private int from;
private int to;
public TaskDemo(int from, int to) {
super();
this.from = from;
this.to = to;
}
@Override
protected Integer compute() {
//判断任务大小是否合适(是否到达范围)
if (THRESHOLD > (to - from)) {
//若是,则进行汇总统计
return IntStream.range(from, to + 1)
.reduce((a, b) -> a + b)
.getAsInt();
} else {//否则继续拆分
int forkNumber = (from + to) / 2;
System.out.println(String.format("拆分%d - %d ==> %d ~ %d, %d~%d",from,to,from,forkNumber,forkNumber+1,to));
TaskDemo left = new TaskDemo(from, forkNumber);
TaskDemo right = new TaskDemo(forkNumber + 1, to);
// fork()方法:将子任务放入队列并安排异步执行
left.fork();
right.fork();
// invokeAll(left,right);
//分别拿到两个子任务的值
return left.join() + right.join();//阻塞当前线程并等待获取结果
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//在 Java 8 中,创建 ForkJoinPool 实例的最简单的方式就是使用其静态方法。commonPool()提供了对公共池的引用,公共池是每个 ForkJoinTask 的默认线程池。
//使用预定义的公共池可以减少资源消耗,因为它会阻止每个任务创建一个单独的线程池。
ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
ForkJoinTask result = forkJoinPool.submit(new TaskDemo(1, 1000));
System.out.println("计算结果为"+result.get());
forkJoinPool.shutdown();
}
}
ForkJoinPool很适合执行计算密集型的任务(拆分大任务再汇总小任务计算结果),若拆分逻辑比计算逻辑还要复杂时,ForkJoinPool并不会带来性能的提升,反而可能会起到负面作用。