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 类型,我们将做两个测试
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)
是一样的情况吗?下面就来测试
调用代码如下
testThreadPool(new ForkJoinPool(2));
执行后的效果是每次都能把所有任务执行完,输出类似如下:
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 不会像普通线程池那样被死锁的秘诀。
我们断点调试观察一下内部状态,自然,最好的理解还是阅读源代码。下面依次截了三个图,它们来自同一次运行的前后
断点停在 "waiting" 行时的 ForkJoinPool 线程池内容状态
工作队列的数量为 3,正在运行的任务数为 2
断点停在第二次 outertask.get() 行时
工作队列的数量变成了 5,threadPool 的 size 为 3,看到 steals 窃取了任务数为 4
断点停在 "done" 行时
任务全部完成,工作队列的数量变成了4
本文对 Java 普通线程池与 ForkJoinPool 的一个简单对比旨在提供了一种避免任务相互等待的可能性。也能从感性上对 ForkJoinPool 一点浅显的认识。