ForkJoinPool如何支持递归调用的

为了缩短篇幅,在源码的截图中,只截取部分关键代码,同时会把代码的行数也截出来,方便在源码中定位(不同版本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 的源码。

在这里插入图片描述

  • 12
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值