CompletableFuture 到底该怎么用:从方法、技巧、踩坑到源码

CompletableFuture 到底该怎么用:从方法、技巧、踩坑到源码 很多人第一次接触 CompletableFuture,感觉它很优雅:链式调用、异步编排、异常处理、多个任务聚合,看起来比手写线程池高级得多。 但真到项目里,问题往往不是“不会用”,而是“以为自己会用”。 比如: 为什么有的回调在主线程执行,有的又跑到了线程池里? 为什么 thenApply() 和 thenApplyAsync() 只差一个 Async,行为却可能完全不同? 为什么我明明用了异步,结果接口还是卡住了? 为什么 allOf() 执行完了,结果还得自己一个个 join() 去拿? 为什么异常被包成了 CompletionException,日志看起来特别恶心? 为什么线上机器 CPU 很高,最后发现是把阻塞 IO 全扔进了 ForkJoinPool.commonPool()? 这篇文章不打算只列 API,而是把 CompletableFuture 真正在工程里怎么用、怎么避坑、JDK 为什么这么设计,以及核心源码怎么运作,一次讲透。 一、先建立一个正确心智模型 很多人把 CompletableFuture 理解成“异步线程工具”,这个说法不算错,但太浅了。 更准确一点,它其实同时做了两件事: 它是一个 Future,代表“未来某个时刻会完成的结果”。 它是一个 CompletionStage,代表“这个结果完成之后,还能继续挂后续动作的可编排节点”。 也就是说,CompletableFuture 不只是“拿结果”,更重要的是“描述依赖关系”。 你可以把它想成一个节点: 节点里最终会有一个结果,或者一个异常。 节点完成后,会触发依赖它的后续节点。 节点和节点之间可以串行、并行、汇聚、竞争。 这才是它和传统 Future 最大的区别。 传统 Future 的典型写法是: Future<Order> future = executor.submit(this::queryOrder); Order order = future.get(); 问题在于: get() 是阻塞的。 不能优雅地声明“拿到订单后再查用户”。 多个 Future 的组合很难写。 而 CompletableFuture 的核心价值,就是把“等待结果”和“结果完成后的动作”拆开。 二、最常用的几类 API,到底该怎么分 1. 创建任务 最常见的是这两个: ...

April 26, 2026 · 5 min · 990 words · WY

JDK线程、虚拟线程、Go协程与Netty Event Loop:调度模型和阻塞队列全景解析

目录 目录 引子:这几个东西为什么总被拿来一起比较 先统一一个视角:它们本质上都在解决调度问题 JDK 平台线程:1:1 映射内核线程 JDK 虚拟线程:M:N 调度,但语义仍然是 Thread Go 协程:语言运行时主导的 GMP 调度模型 Netty Event Loop:少量线程驱动大量连接 把四者放在一张图里看 它们之间到底是什么关系 一个关键问题:阻塞到底意味着什么 JDK 阻塞队列全景梳理 ArrayBlockingQueue LinkedBlockingQueue SynchronousQueue PriorityBlockingQueue DelayQueue LinkedTransferQueue LinkedBlockingDeque 阻塞队列之间的核心区别 这些队列和线程模型怎么配合 怎么选:按场景给结论 最后总结 引子:这几个东西为什么总被拿来一起比较 很多人第一次接触这几个概念时,会感觉它们像是同一层面的东西: JDK 线程 JDK 虚拟线程 Go 协程 Netty Event Loop JDK 阻塞队列 但它们其实分布在不同层次: 线程 / 协程 / 虚拟线程 解决的是执行单元怎么表示 调度器 解决的是谁来跑、什么时候跑 Event Loop 解决的是I/O 事件如何复用少量线程 阻塞队列 解决的是任务或数据如何在并发单元之间传递 把这些层次混在一起,就很容易得出错误结论,比如: “虚拟线程就是 Java 版协程” “Netty loop 就是协程调度器” “用了虚拟线程,就不需要线程池和队列了” “所有生产者消费者问题都用 LinkedBlockingQueue 就行” 这篇文章的目标,就是把这些东西放到一张统一地图里。 ...

April 22, 2026 · 5 min · 905 words · WY

线程池里用 InheritableThreadLocal 传值,怎么就丢了?

线程池里用 InheritableThreadLocal 传值,怎么就丢了? 你肯定遇到过这种怪事:代码里明明用了 InheritableThreadLocal,信心满满地觉得子线程能自动继承主线程的变量,结果一跑,值没了,或者串了。 我第一次碰到这问题时, debug 了一下午,最后发现是线程池的锅。今天我就把当时挖出来的东西从头捋一遍,包括 Java 源码里到底是怎么拷贝的,还有阿里那个 TransmittableThreadLocal 到底是怎么救场的。 先看 InheritableThreadLocal 是怎么“继承”的 每个 Java 线程,也就是 Thread 类的实例,肚子里都揣着两个 ThreadLocalMap: 一个叫 threadLocals,专门给普通的 ThreadLocal 存变量。 另一个叫 inheritableThreadLocals,给 InheritableThreadLocal 用。 这两个 map 平时都是懒加载的,你第一次 set 的时候才会创建。 当我们 new 一个子线程的时候,事情就发生在 Thread 的 init() 方法里。这个方法会在子线程的构造函数中被调用,关键的一段逻辑是这样的(我摘了简化版的源码): if (parent.inheritableThreadLocals != null) { this.inhertableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); } 也就是说,如果父线程(也就是创建子线程的那个线程)的 inheritableThreadLocals 里面有东西,就把整个 map 拷贝一份,作为子线程的 inheritableThreadLocals。 那 createInheritedMap 是怎么拷贝的呢?它实际上是调用了 ThreadLocalMap 的一个特殊构造方法: private ThreadLocalMap(ThreadLocalMap parentMap) { // ... 一些初始化代码 ... for (int i = 0; i < len; i++) { Entry e = parentMap.table[i]; if (e != null) { ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null) { // 注意这一行:调用了 childValue 方法 Object value = key.childValue(e.value); Entry c = new Entry(key, value); // ... 把新 entry 放到子线程的 map 里 ... } } } } 重点就是这个 childValue 方法。它是 InheritableThreadLocal 里定义的一个 protected 方法,默认实现就是直接返回传进来的值: ...

April 15, 2026 · 2 min · 336 words · WY