为了缩短篇幅,在源码的截图中,只截取部分关键代码,同时会把代码的行数也截出来,方便在源码中定位(不同版本JDK可能有些许不同)。
文章关于 ForkJoinPool 任务如何调度执行等未讨论,这不在本文的篇幅内。
下面是分别使用ThreadPoolExecutor线程池和ForkJoinPool线程池执行的一段递归代码。两段代码只有线程池实现不同。
(测试 CPU 是 12 线程,openJDK21)
问题
ThreadPoolExecutor:
public class Main {
static final ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() - 1);
public static void main(String[] args) throws Exception {
System.out.println(dfs(0));
}
static int dfs(int n) throws ExecutionException, InterruptedException {
if (n == 100) {
return n;
}
Future<Integer> submit = executorService.submit(() -> {
try {
System.out.println(Thread.currentThread());
return dfs(n + 1);
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
});
return submit.get() + n;
}
}
得到以下输出:
ForkJoinPool:
public class Main {
static final ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
public static void main(String[] args) throws Exception {
System.out.println(dfs(0));
}
static int dfs(int n) throws ExecutionException, InterruptedException {
if (n == 100) {
return n;
}
ForkJoinTask<Integer> submit = forkJoinPool.submit(() -> {
try {
System.out.println(Thread.currentThread());
return dfs(n + 1);
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
});
return submit.get() + n;
}
}
可以得到正确答案:
通过上面的代码可以发现 ForkJoinPool 支持递归调用,而 ThreadPoolExecutor 不支持递归调用。这是为什么喃?
问题在于它们调用 submit() 后返回的 Future 实现不同。
通过查看源码可以发现 ThreadPoolExecutor 返回的 Future 实现为 FutureTask,ForkJoinPool 返回的实现为 ForkJoinTask。Future 的实现不同导致它们在调用 Future.get() 有不同的逻辑。
FutureTask 的实现
先看看 FutureTask 的 get() 方法:
FutureTask.get()
FutureTask.awaitDone() 部分代码
我们只需要关注几个重点就行了。可以看到 get() 方法在任务没有执行完成时会调用 awaitDone() 方法。而 awaitDone() 方法会在最后调用 LockSupport.park(this) 方法(这里忽略具体执行流程,从大的结果上看),而 LockSupport.park(this) 正是调用 Unsafe.park 方法使当前线程挂起,这很符合我们对线程池的理解,拿不到结果就挂起。
也正是因为线程会挂起,所以在第一段测试代码中只打印了 11 行结果,这时线程池中的 11 个线程都挂起了,所以也就没办法继续执行下去了。
ForkJoinTask 的实现
看看 ForkJoinTask
ForkJoinTask.get()
可以看到 get() 方法中也有 awaitDone() 的调用。
ForkJoinTask.awaitDone 部分代码
需要关注的代码是红框中的代码和高亮的 tryRemoveAndExec() 方法。
红框中的代码对 ForkJoinWorkerThread 做了一些特殊处理,这是 ForkJoinPool 使用的线程类型。此时 q 指向当前线程在 ForkJoinPool 中创建的任务队列 WorkQueue,p 指向当前线程所属的 ForkJoinPool。
tryRemoveAndExec() 方法是实现递归的关键。
ForkJoinPool.WorkQueue.tryRemoveAndExec 方法
可以看到 tryRemoveAndExec 会直接调用 task.doExec(),而 task 不就是待执行的任务吗,直接调用相当于直接进行了递归调用。
细心的同学可能已经发现了,在最初的测试代码输出的截图中打印的线程名都是同一个,这就是因为它们是在同一个线程中执行的。
如果执行过测试代码的同学也会发现,打印的线程在最开始的并不是同一个,这是因为 ForkJoinPool 在初始化线程时窃取任务。
ForkJoinPool 测试打印的开头:
总结
ForkJoinPool 支持递归主要是因为 ForkJoinTask 的 get 方法对 ForkJoinWorkThread 进行了一些特殊的处理,使它能在递归调用时直接执行需要执行的任务,而不是将它提交到线程池中由其它线程执行。这样避免了很长的等待链,而这个链可能因为线程不够永远都不会返回,造成了死锁。
在 CompletableFuture 中也有类似的实现,有兴趣的同学可以结合本文跟一下 CompletableFuture 的源码。