Java 普通线程池与 ForkJoinPool 的效果对比
Java 多线程编程常用的一个接口是
关于什么时候用
关于 Java7 的 fork-join 框架可参考很多年前的一篇 Java 的 fork-join 框架实例备忘。ForkJoinPool 的一个典型特征是能够进行 Work stealing。它也是 Akka actor 效率高效的一个有力保证。
本文只能某一种情形下在选择普通线程池与 ForkJoinPool 的区别,直接说吧,普通线程更容易造成死锁,而 ForkJoinPool 却能应对相同的状况。
以下面代码为例,testThreadPool(..) 可接收不同的 ExecutorService 类型,我们将做两个测试

如果加一个断点在

一个普通线程池只有一个工作队列
那么换成
执行后的效果是每次都能把所有任务执行完,输出类似如下:
ForkJoinPool 与普通线程池的主要区别前面提到过的,它实现了工作窃取算法。明显的内部区别是
这就是 ForkJoinPool 不会像普通线程池那样被死锁的秘诀。
我们断点调试观察一下内部状态,自然,最好的理解还是阅读源代码。下面依次截了三个图,它们来自同一次运行的前后

工作队列的数量为 3,正在运行的任务数为 2

工作队列的数量变成了 5,threadPool 的 size 为 3,看到 steals 窃取了任务数为 4

任务全部完成,工作队列的数量变成了4
本文对 Java 普通线程池与 ForkJoinPool 的一个简单对比旨在提供了一种避免任务相互等待的可能性,特别是在任务中又提交子任务然后等待子任务时的情况。也能从感性上对 ForkJoinPool 一点浅显的认识。 永久链接 https://yanbin.blog/common-threadpool-vs-forkjoinpool/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
ExecutorService, 其实就一个线程池的接口,一般由两种方式创建线程池,一为 Executors 的工厂方法,二则创建 ForkJoinPool 实例,当然也有直接使用 ThreadPoolExecutor 的。关于什么时候用
ForkJoinPool 或普通的线程池(如 Executors.newFixedThreadPool(2) 或 new ThreadPoolExecutor(...)) 不过多的述说。如果要运用到 ForkJoinTask 的话就要用 ForkJoinPool, 它是 Java7 新引入的线程池类型。关于 Java7 的 fork-join 框架可参考很多年前的一篇 Java 的 fork-join 框架实例备忘。ForkJoinPool 的一个典型特征是能够进行 Work stealing。它也是 Akka actor 效率高效的一个有力保证。
本文只能某一种情形下在选择普通线程池与 ForkJoinPool 的区别,直接说吧,普通线程更容易造成死锁,而 ForkJoinPool 却能应对相同的状况。
以下面代码为例,testThreadPool(..) 可接收不同的 ExecutorService 类型,我们将做两个测试
1private static void testThreadPool(ExecutorService threadPool) {
2 Future[] outerTasks = IntStream.rangeClosed(1, 2).mapToObj(i ->
3 threadPool.submit(() -> {
4 System.out.println(Thread.currentThread().getName() + ", level1 task " + i);
5
6 Future<?> innerTask = threadPool.submit(() ->
7 System.out.println(Thread.currentThread().getName() + ", level2 task" + i));
8
9 try {
10 innerTask.get();
11 } catch (Exception e) {
12 e.printStackTrace();
13 }
14 })).toArray(Future[]::new);
15
16 System.out.println("waiting...");
17 try {
18 for (Future<?> outerTask : outerTasks) {
19 outerTask.get();
20 }
21 } catch (Exception e) {
22 e.printStackTrace();
23 }
24 System.out.println("done");
25}普通线程池测试
调用代码如下testThreadPool(Executors.newFixedThreadPool(2);那么我们永远等不到执行结果,不能到达 "done" 那一行,控制台的输出停在
waiting...因为线程池占满了,永远得不到空闲的线程来执行 "level2 task"。线程状态可以看到线程池中的两个线程都是 "WAITING (parking)" 状态。简单用下图分析一下为什么产生死锁状态
pool-1-thread-2, level1 task 2
pool-1-thread-1, level1 task 1

- 首先提交的两个任务把线程池中的两个线程都占满了,而它们又分别提交了子任务,并等待子任务完成才退出
- 子任务在工作队列中等待线程池中释放出空闲线程来执行,这是不可能的,所以两边互相等待,死锁了
如果加一个断点在
innerTask.get() 处,可以看到下面的效果
一个普通线程池只有一个工作队列
那么换成
new ForkJoinPool(2) 是一样的情况吗?下面就来测试测试 ForkJoinPool
调用代码如下testThreadPool(new ForkJoinPool(2));或者
testThreadPool(Executors.newWorkStealingPool(2));上面两种方式得到的都是 ForkJoinPool,另外用 ForkJoinPool.commonPool() 也是,只不过它的线程池大小由机器的 CPU 内核决定的。
执行后的效果是每次都能把所有任务执行完,输出类似如下:
waiting...是不是瞬间感觉到 ForkJoinPool 比普通线程池强大啊,也许这也是为什么 Java8 Stream 的
ForkJoinPool-1-worker-0, level1 task 2
ForkJoinPool-1-worker-1, level1 task 1
ForkJoinPool-1-worker-2, level2 task2
ForkJoinPool-1-worker-2, level2 task1
done
parallelStream() 或者 CompletableFuture.runAsync() 类似的方法未指定线程池时使用的默认线程池就是 ForkJoinPool#commonPool(),因为它不会死锁。ForkJoinPool 与普通线程池的主要区别前面提到过的,它实现了工作窃取算法。明显的内部区别是
- 普通线程池所有线程共享一个工作队列,有空闲线程时工作队列中的任务才能得到执行
- ForkJoinPool 中的每个线程有自己独立的工作队列,每个工作线程运行中产生新的任务,放在队尾
- 某个工作线程会尝试窃取别个工作线程队列中的任务,从队列头部窃取
- 遇到 join() 时,如前面的 future.get(),如果 join 的任务尚未完成,则可先处理其他任务
这就是 ForkJoinPool 不会像普通线程池那样被死锁的秘诀。
我们断点调试观察一下内部状态,自然,最好的理解还是阅读源代码。下面依次截了三个图,它们来自同一次运行的前后

断点停在 "waiting" 行时的 ForkJoinPool 线程池内容状态
工作队列的数量为 3,正在运行的任务数为 2

断点停在第二次 outertask.get() 行时
工作队列的数量变成了 5,threadPool 的 size 为 3,看到 steals 窃取了任务数为 4

断点停在 "done" 行时
任务全部完成,工作队列的数量变成了4
本文对 Java 普通线程池与 ForkJoinPool 的一个简单对比旨在提供了一种避免任务相互等待的可能性,特别是在任务中又提交子任务然后等待子任务时的情况。也能从感性上对 ForkJoinPool 一点浅显的认识。 永久链接 https://yanbin.blog/common-threadpool-vs-forkjoinpool/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。