Java 普通线程池与 ForkJoinPool 的效果对比

Java 多线程编程常用的一个接口是 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 类型,我们将做两个测试

普通线程池测试

调用代码如下

testThreadPool(Executors.newFixedThreadPool(2);

那么我们永远等不到执行结果,不能到达 "done" 那一行,控制台的输出停在

waiting...
pool-1-thread-2, level1 task 2
pool-1-thread-1, level1 task 1

因为线程池占满了,永远得不到空闲的线程来执行 "level2 task"。线程状态可以看到线程池中的两个线程都是 "WAITING (parking)" 状态。简单用下图分析一下为什么产生死锁状态

  1. 首先提交的两个任务把线程池中的两个线程都占满了,而它们又分别提交了子任务,并等待子任务完成才退出
  2. 子任务在工作队列中等待线程池中释放出空闲线程来执行,这是不可能的,所以两边互相等待,死锁了

如果加一个断点在 innerTask.get() 处,可以看到下面的效果

一个普通线程池只有一个工作队列

那么换成 new ForkJoinPool(2) 是一样的情况吗?下面就来测试

测试 ForkJoinPool

调用代码如下

testThreadPool(new ForkJoinPool(2));

或者

testThreadPool(Executors.newWorkStealingPool(2));

上面两种方式得到的都是 ForkJoinPool,另外用 ForkJoinPool.commonPool() 也是,只不过它的线程池大小由机器的 CPU 内核决定的。

执行后的效果是每次都能把所有任务执行完,输出类似如下:

waiting...
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

是不是瞬间感觉到 ForkJoinPool 比普通线程池强大啊,也许这也是为什么 Java8 Stream 的 parallelStream() 或者 CompletableFuture.runAsync() 类似的方法未指定线程池时使用的默认线程池就是 ForkJoinPool#commonPool(),因为它不会死锁。

ForkJoinPool  与普通线程池的主要区别前面提到过的,它实现了工作窃取算法。明显的内部区别是

  1. 普通线程池所有线程共享一个工作队列,有空闲线程时工作队列中的任务才能得到执行
  2. ForkJoinPool 中的每个线程有自己独立的工作队列,每个工作线程运行中产生新的任务,放在队尾
  3. 某个工作线程会尝试窃取别个工作线程队列中的任务,从队列头部窃取
  4. 遇到 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 Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments