【Future vs CompletableFuture vs JS Promise】

一、Future:java.util.concurrent.Future vs CompletableFuture

1. 线程池 ExecutorService.submit() 返回的传统 Future 为什么能中断线程?

传统 FutureTask(线程池底层实现类)持有任务与执行线程的引用 ,这是和 CompletableFuture 最核心的区别:

  1. 当线程池把任务丢给工作线程执行时,工作线程会绑定当前 FutureTask 对象;
  2. FutureTask 内部有成员变量 runner,用来记录正在执行该任务的线程
  3. 调用 future.cancel(true) 时,底层逻辑:
    • 判断任务是否正在运行;
    • 如果 runner 不为 null,直接调用 runner.interrupt(),给目标线程发送中断标记;
    • 再把任务状态置为「取消」。

核心:传统 FutureTask 有 runner 字段存执行线程引用

源码简化示意:

java 复制代码
public class FutureTask<T> implements RunnableFuture<T> {
    private volatile Thread runner; // 正在执行此任务的线程引用
    public boolean cancel(boolean mayInterruptIfRunning) {
        if (mayInterruptIfRunning) {
            Thread r = runner;
            if (r != null)
                r.interrupt(); // 直接中断执行线程
        }
        return true;
    }
}

所以传统 Future 不是"凭空中断",它保存了执行任务的线程引用 ,才有能力调用 interrupt()


2. CompletableFuture 为什么不保存执行线程?

CompletableFuture 的设计定位是通用结果容器(Promise),不绑定任何执行线程:

  1. 手动 new CompletableFuture<>() 场景:没有固定任务线程,任意线程都能调用 complete(),根本不存在唯一执行线程;
  2. supplyAsync/runAsync 场景:只是把任务交给线程池执行,CompletableFuture 不会记录、持有执行这个任务的线程;
  3. 多任务竞速场景(你之前书中双任务示例):多个线程都能尝试 complete,没有单一归属线程。

它内部没有类似 runner 的线程引用字段 ,调用 cancel() 时,找不到任何线程去执行 interrupt(),只能单纯修改自身状态,标记为 CancellationException

同时 cancel(boolean mayInterruptIfRunning) 的布尔参数完全失效,传 true/false 行为一模一样。


二、一句话对比两者 cancel 完整逻辑

1. 传统 Future(FutureTask)

  • 持有执行线程引用 runner
  • cancel(true)
    1. 修改任务状态为取消;
    2. 拿到 runner 线程,调用 thread.interrupt()
    3. 后续阻塞 get() 抛出 CancellationException
  • cancel(false):只改状态,不发中断,等任务自然跑完。

2. CompletableFuture

  • 不持有任何执行线程引用
  • cancel(任意布尔值)
    1. 仅修改自身状态为异常完成,内部存入 CancellationException
    2. 不会执行任何线程中断操作,后台任务会继续跑完;
    3. 后续 get/join/回调 都会感知到取消异常。

三、延伸:如果需要真正中断 CompletableFuture 的任务该怎么做?

既然 cancel() 没用,只能业务侧自行控制:

  1. 方案1:线程中断标记判断
    任务内循环读取 Thread.currentThread().isInterrupted(),外部手动拿到线程调用 interrupt()(不能靠 f.cancel());

  2. 方案2:自定义 volatile 开关标记

    java 复制代码
    volatile boolean stop = false;
    CompletableFuture.runAsync(() -> {
        while (!stop) { ...业务逻辑... }
    });
    // 外部终止任务
    stop = true;
  3. 方案3:超时自动放弃 orTimeout()

    java 复制代码
    // 3秒未完成则抛出超时异常,仅改变Future状态,不会停止任务
    f.orTimeout(3, TimeUnit.SECONDS);
  4. 替代方案:改用线程池 FutureTask
    如果需求是"随时可中断运行中任务",放弃纯 CompletableFuture,用 executor.submit(Callable) 获取传统 Future。


一、Java CompletableFuture ≈ JS Promise

二者底层设计思想几乎同源,都是异步可外部决议的模型,写法、能力一一对应:

1. 核心概念对照表

Java CompletableFuture JavaScript Promise 作用
new CompletableFuture<T>() new Promise((resolve, reject) => {}) 手动创建可外部完成的异步容器
f.complete(val) resolve(val) 成功决议,填入正常结果
f.completeExceptionally(ex) reject(err) 异常决议,抛出错误
thenAccept(r -> {}) .then(res => {}) 消费结果,无返回值
thenApply(r -> newVal) .then(res => transformRes) 转换结果,生成新异步对象
whenComplete((res, err) -> {}) .finally() 无论成功失败都会执行收尾
exceptionally(ex -> fallback) .catch(err => fallback) 捕获异常,返回降级默认值
CompletableFuture.anyOf() Promise.race() 多个异步,取最先完成的那个
CompletableFuture.allOf() Promise.all() 等待所有异步全部完成

双任务竞速取最快结果 的案例,完全等价 JS 的 Promise.race,逻辑一模一样。

2. 编码范式一致:订阅优先

JS 标准写法:先注册 .then() / .catch(),再执行 resolve/reject

javascript 复制代码
// JS
const p = new Promise((resolve) => {
  setTimeout(() => resolve(100), 500);
});
// 先注册回调
p.then(res => console.log(res));

Java CompletableFuture 标准写法:先 thenAccept 注册回调,再子线程调用 complete

java 复制代码
// Java
var f = new CompletableFuture<Integer>();
// 先注册回调
f.thenAccept(res -> System.out.println(res));
new Thread(() -> {
    Thread.sleep(500);
    f.complete(100);
}).start();

3. 阻塞 vs 异步等待对应

  • Java f.get() / f.join() ≈ JS 顶层 await f
    二者都是同步阻塞等待异步结果
    走到等待代码时,如果异步已经完成,直接返回,无阻塞。
javascript 复制代码
// JS await 等价 Java join/get
async function test() {
  const p = new Promise(resolve => setTimeout(() => resolve(100), 500));
  // 先执行一堆耗时逻辑
  await new Promise(r => setTimeout(r, 1000));
  // 任务早就完成,await 直接拿到结果不等待
  const res = await p;
}

二、关键差异(别踩坑)

虽然用法像,但语言底层模型有本质区别:

  1. 线程模型完全不同
    • JS:单线程事件循环,异步只是回调排队,不存在多线程并发竞争;
    • Java:多线程操作系统内核线程,complete() 存在多线程竞态,需要考虑线程安全。
  2. 回调线程可控性
    • JS:回调永远在主线程事件循环执行;
    • Java:thenAccept 在执行 complete 的线程运行;thenAcceptAsync 可自定义线程池,灵活控制回调执行线程。
  3. 异常机制区分
    • JS:reject 后必须 .catch,否则全局 unhandledRejection 报错;
    • Java:未捕获异常只会存放在 Future 内部,不主动抛出,只有调用 get/join 才会触发异常。
  4. 生命周期控制
    • Java 可以重复调用 complete(只会第一次生效);JS 的 resolve/reject 调用多次无任何效果,行为一致。

三、补充:为什么 Java 8 要借鉴 Promise 设计 CompletableFuture

在 Java 8 之前普通 Future 是残缺的:

只能阻塞等待,无法链式回调、不能外部手动完成,相当于只有 JS Promise 一半功能。

CompletableFuture 直接借鉴了主流异步模型 Promise 的设计思路,补上了异步编排、手动决议、多任务组合的短板,所以写起来语法和思维和 JS 异步高度趋同。

如果你平时写前端 JS,上手 CompletableFuture 会非常顺滑,唯一需要额外注意的就是 Java 多线程并发安全问题。