转载

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 类型,我们将做两个测试

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)" 状态。简单用下图分析一下为什么产生死锁状态

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

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

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

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

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

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

测试 ForkJoinPool

调用代码如下

 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  与普通线程池的主要区别前面提到过的,它实现了工作窃取算法。明显的内部区别是

  1. 普通线程池所有线程共享一个工作队列,有空闲线程时工作队列中的任务才能得到执行
  2. ForkJoinPool 中的每个线程有自己独立的工作队列,每个工作线程运行中产生新的任务,放在队尾
  3. 某个工作线程会尝试窃取别个工作线程队列中的任务,从队列头部窃取
  4. 遇到 join() 时,如前面的 future.get(),如果 join 的任务尚未完成,则可先处理其他任务

这就是 ForkJoinPool 不会像普通线程池那样被死锁的秘诀。

我们断点调试观察一下内部状态,自然,最好的理解还是阅读源代码。下面依次截了三个图,它们来自同一次运行的前后

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

断点停在 "waiting" 行时的 ForkJoinPool 线程池内容状态

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

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

断点停在第二次 outertask.get() 行时

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

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

断点停在 "done" 行时

任务全部完成,工作队列的数量变成了4

本文对 Java 普通线程池与 ForkJoinPool 的一个简单对比旨在提供了一种避免任务相互等待的可能性。也能从感性上对 ForkJoinPool 一点浅显的认识。

原文  https://yanbin.blog/common-threadpool-vs-forkjoinpool/
正文到此结束
Loading...