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 类型,我们将做两个测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
private static void testThreadPool(ExecutorService threadPool) { Future[] outerTasks = IntStream.rangeClosed(1, 2).mapToObj(i -> threadPool.submit(() -> { System.out.println(Thread.currentThread().getName() + ", level1 task " + i); Future<?> innerTask = threadPool.submit(() -> System.out.println(Thread.currentThread().getName() + ", level2 task" + i)); try { innerTask.get(); } catch (Exception e) { e.printStackTrace(); } })).toArray(Future[]::new); System.out.println("waiting..."); try { for (Future<?> outerTask : outerTasks) { outerTask.get(); } } catch (Exception e) { e.printStackTrace(); } System.out.println("done"); } |
普通线程池测试
调用代码如下
testThreadPool(Executors.newFixedThreadPool(2);
那么我们永远等不到执行结果,不能到达 "done" 那一行,控制台的输出停在
waiting...
pool-1-thread-2, level1 task 2
pool-1-thread-1, level1 task 1
因为线程池占满了,永远得不到空闲的线程来执行 "level2 task"。线程状态可以看到线程池中的两个线程都是 "WAITING (parking)" 状态。简单用下图分析一下为什么产生死锁状态
- 首先提交的两个任务把线程池中的两个线程都占满了,而它们又分别提交了子任务,并等待子任务完成才退出
- 子任务在工作队列中等待线程池中释放出空闲线程来执行,这是不可能的,所以两边互相等待,死锁了
如果加一个断点在 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 与普通线程池的主要区别前面提到过的,它实现了工作窃取算法。明显的内部区别是
- 普通线程池所有线程共享一个工作队列,有空闲线程时工作队列中的任务才能得到执行
- 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 Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。