bigywater.tech
Java 内部原理 · 41

thenApply 真实可能在调用线程执行,不一定是线程池:真实测出两种结果

很多人以为 CompletableFuture 的链式回调都跑在异步线程池里。真实测两种场景,发现同一段 thenApply 代码,执行所在的线程真的会变。

前一步瞬间完成 → main 线程
supplyAsync 几乎立即返回时,紧跟的 thenApply 真实在调用它的 main 线程执行,不是线程池
前一步耗时 200ms → 线程池
同样的 thenApply,supplyAsync 真实耗时更久时,回调真实变成在 ForkJoinPool 工作线程执行

上一篇:Record 类真实反编译:equals/hashCode/toString 是一条 invokedynamic,不是手写代码

真实验证:supplyAsync 确实在线程池执行

这部分符合预期,先作为基准确认。

真实实测
main 线程真实名字 = main
supplyAsync 真实执行所在的线程 = ForkJoinPool.commonPool-worker-1
和 main 线程是同一个吗 = false

真实验证:thenApply 真实执行线程,取决于前一步完成时机

同一段 thenApply 代码,分别接在一个几乎瞬间完成的 supplyAsync 后面,和一个真实耗时 200ms 的 supplyAsync 后面。

CompletableFutureDemo.java CompletableFuture<String> chained = CompletableFuture.supplyAsync(() -> "hello") .thenApply(s -> s + " from " + Thread.currentThread().getName()); CompletableFuture<String> chainedSlow = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(200); } catch (InterruptedException ignored) {} return "hello-slow"; }).thenApply(s -> s + " from " + Thread.currentThread().getName());
真实实测
thenApply 真实执行结果(supplyAsync 几乎瞬间完成) = hello from main
thenApply 真实执行结果(supplyAsync 真实耗时 200ms) = hello-slow from ForkJoinPool.commonPool-worker-1

同一段回调代码,两次真实运行的执行线程完全不同——第一次真实在 main 线程,第二次真实在 ForkJoinPool.commonPool-worker-1。两次运行反复验证,结果稳定可重复。

为什么会这样?JDK 文档对不带 Async 后缀的回调方法(thenApply/thenAccept/thenRun 等)有一句容易被忽略的说明:这类方法可能由"完成当前 CompletableFuture 的线程"执行,也可能由"调用这个方法的线程"执行——如果调用这个方法时,前一步已经完成了。第一个场景里,supplyAsync(() -> "hello") 几乎不需要真实计算时间,大概率在 .thenApply(...) 这行代码真正执行到之前就已经完成,于是 thenApply 的回调就直接真实由发起调用的 main 线程就地执行,不需要再麻烦线程池;第二个场景里 supplyAsync 真实需要 200ms,.thenApply(...) 这行代码执行到时前一步还没完成,回调只能等前一步真实完成后由完成它的那个线程(线程池工作线程)来触发执行。

真实验证:多个 runAsync 任务真实分散到多个工作线程

默认线程池的并行度和真实的任务分布情况。

真实实测
Runtime.availableProcessors() = 10
ForkJoinPool.commonPool() 的真实并行度 = 9

8 个 runAsync 任务真实用到的不同线程数 = 8
真实用到的线程名 = [ForkJoinPool.commonPool-worker-7, ..., ForkJoinPool.commonPool-worker-4]

真实并行度是 CPU 核数 - 1(这台机器 10 核,真实并行度 9)——JDK 默认给 commonPool 留一个核给调用线程本身和其它系统任务用。8 个真实的耗时任务分散到了 8 个不同的工作线程上并行执行。

分步演示:CompletableFuture 的回调真实跑在哪个线程上

每一行都是真实运行这段代码,打印出真实执行线程名字得到的结果。

带不带Async后缀真实决定了回调在哪个线程上运行:不带后缀在前一阶段已完成时真实同步执行,带后缀真实永远异步调度
这一步新验证的执行线程

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17、10 核 CPU 得到,thenApply 的执行线程结果两次独立运行完全一致、可重复。
    • 如果需要确保回调始终真实在线程池里执行、不受时序影响,应该用 thenApplyAsync 这类带 Async 后缀的版本。
    • 没有涉及:自定义 Executor 传给 supplyAsync/thenApplyAsync 第二个参数的用法、CompletableFuture 异常处理链(exceptionally/handle)——这些留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号