ForkJoinPool
ForkJoinPool
是什么?和ExectuorService
有什么差异?
和ExectuorService
相同的点在于,可以驱动并完成多个提交任务。特点如下:
-
ForkJoinPool
适用于提交会产生子任务的任务。文字上似乎很难理解这是什么含义。看下面的图片:
image.png
这个提交的task
本身会产生很多个子task(sub-task)
,那么这样的场景就是和使用ForkJoinPool
。task
会产生很多子task(sub-task)
,如果都在一个线程上完成的话,那么cpu将不能得到有效利用(为什么?参见这里关于CPU-线程模型的一些知识点)。说了这些还是感觉很抽象,那么有没有具体的例子可以作为理解什么样的task
会产生多个子task(sub-task)
呢?斐波那契数列(Fibonacci Sequence
)。除了第一个和第二个数,每一个斐波那契数都可以被拆成前两个数的和。那么这样就可以将某个斐波那契数拆成前两个斐波那契数和的task。可以参考把原来需要递归调用的方法,放到多个线程去执行。
如果还是不明白没,希望下面这张图能帮你理解ForkJoinPool
对于task
本身会产生很多个子task(sub-task)
这一概念。
- Per-Thread queueing & Working-Stealing 线程的任务队列和工作任务协同分担。最接近的翻译应该是: 线程对应的任务队列和工作任务盗取,显然这样翻译对于理解概念并没有什么额外帮助(当然第一种翻译也没有什么理解上的帮助...my poor English)。
还是通过举例子来理解这一概念吧。假设有一个两个线程的线程池,向里面提交了很多task:
image.png
这两个线程本身就各自有一个deque (双端队列:double-ended queue)
用于存储某个task fork出来的多个子task(并不会存储到上面的common queue
)中:
fork-task.png
这样做有什么好处呢?
- 线程只需要不停的从自己的
deque
中获取任务就行,提高了线程利用率,不需要停下来的从外部获取task,不会产生阻塞(例外:Working-Stealing时除外,这个稍后还会讲到) - 由于减少了不同任务的线程调度,所以降低切换线程的性能损耗
那这样做有没有什么问题呢?
- 如果
thread-1
某个task拆分出了很多很多个子task,而thread-2
由于子task较少,很快就执行完了。那么thread-2
就会看着thread-1
一直满负载运行,而自己在摸鱼...
这肯定是不够高效的,作为一个积极的打工人,thread-2
应该勇敢的站出来替thread-1
分担部分task。于是就产生了上面提到的Working-Stealing这一概念,可以说是工作偷取,或者工作协同。由于thread-2
需要从thread-1
的deque
尾部获取task,就会涉及到同步的问题,也必然会产生阻塞(上面提到的阻塞例外即来源于此)。
综上,ForkJoinPool
和ExectuorService
有什么差异?相同点都是用于异步并发执行任务,不同点在于ForkJoinPool
的每个线程都有自己的 task deque
用于存储某个task fork出来的子task。并且为了提高线程的利用率,ForkJoinPool
各个线程之间存在子task协同完成这一概念,空闲的线程会通过获取其他线程deque
尾部的子task协助完成任务。
既然说的这么好,那怎么样使用呢? api和ExectuorService
差别不大。但是需要关注submit(ForkJoinTask<T> task)、invoke(ForkJoinTask<T> task) 和 execute(ForkJoinTask<?> task)
。还是拿上面斐波那契数列(Fibonacci Sequence
)的例子:
ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
Integer invoke = forkJoinPool.invoke(new Fibonacci(10));
static class Fibonacci extends RecursiveTask<Integer> {
private int n;
public Fibonacci(int n) {
this.n = n;
}
@Override
protected Integer compute() {
if (n <= 1) {
return n;
}
Fibonacci fibonacci = new Fibonacci(n - 1);
fibonacci.fork();//注意,fork出新的task
Fibonacci fibonacci1 = new Fibonacci(n - 2);
fibonacci1.fork();//注意,fork出新的task
return fibonacci.join() + fibonacci1.join();//join获取结果
}
}
也很简单,那么ForkJoinPool
使用有什么注意点呢?
- 避免在task中出现同步相关的代码
- 避免在不同的task间分享同一个变量
- 不要在代码中出现I/O这样会Block的操作
- 每个task要相对单一和独立
不难看出ForkJoinPool
适用于CPU敏感型的操作需求,注重执行效率。