Java线程(CompletableFuture)

目录

  • Java线程(CompletableFuture)
    • 一、传统Future的四大痛点
      • [1.1 阻塞的 `get()` --- 你只能在门口干等](#1.1 阻塞的 get() — 你只能在门口干等)
      • [1.2 无法组合任务 --- 四个任务得写八行 `get()`](#1.2 无法组合任务 — 四个任务得写八行 get())
      • [1.3 异常处理原始 --- 挂了就挂了,没有Plan B](#1.3 异常处理原始 — 挂了就挂了,没有Plan B)
      • [1.4 无法手动完成 --- 你只能看,不能碰](#1.4 无法手动完成 — 你只能看,不能碰)
    • 二、CompletableFuture四大解决方案
      • [2.1 回调机制 --- 外卖到了,手机自动通知你](#2.1 回调机制 — 外卖到了,手机自动通知你)
      • [2.2 任务编排 --- 声明式组合,一行搞定](#2.2 任务编排 — 声明式组合,一行搞定)
        • [串行依赖 --- 先查用户,再查订单](#串行依赖 — 先查用户,再查订单)
        • [并行合并 --- 同时查库存 + 价格](#并行合并 — 同时查库存 + 价格)
        • [竞速取最快 --- Redis / 本地缓存 / DB,谁快用谁](#竞速取最快 — Redis / 本地缓存 / DB,谁快用谁)
      • [2.3 异常恢复 --- 挂了也能优雅兜底](#2.3 异常恢复 — 挂了也能优雅兜底)
        • [exceptionally --- 最常用的降级模式](#exceptionally — 最常用的降级模式)
        • [handle --- 统一处理正常/异常](#handle — 统一处理正常/异常)
        • [whenComplete --- 类似finally,只记录不干预](#whenComplete — 类似finally,只记录不干预)
      • [2.4 手动完成 --- 任何时候都可以注入结果](#2.4 手动完成 — 任何时候都可以注入结果)
        • [超时兜底 --- 支付接口5秒没返回,手动标记](#超时兜底 — 支付接口5秒没返回,手动标记)
        • [缓存回填 --- 缓存命中直接返回,不等数据库](#缓存回填 — 缓存命中直接返回,不等数据库)
    • 三、实战技巧

Java线程(CompletableFuture)

传统 Future 是 JDK5 引入的异步编程接口,但它有四个致命缺陷。本文用生活场景类比 帮你建立直觉,再用 CompletableFuture 逐一解决。


一、传统Future的四大痛点

1.1 阻塞的 get() --- 你只能在门口干等

调用 future.get() 后,当前线程被挂起,直到任务完成才能继续。

类比:点了一份外卖,你必须站在门口一直等,不能看书、不能洗碗、不能睡觉。

java 复制代码
ExecutorService executor = Executors.newFixedThreadPool(3);
Future<String> delivery = executor.submit(() -> {
    Thread.sleep(3000);
    return "外卖已送达";
});

// 主线程卡死3秒,什么都别想干
String result = delivery.get();
System.out.println(result);
操作 现实世界 Future版本
等待结果 手机通知你 你站门口干等
等待期间 自由做任何事 线程被挂起
结果到了 自动推送 你主动去拿

1.2 无法组合任务 --- 四个任务得写八行 get()

实际业务中很少有孤立任务,更多是:A→B串行依赖A+B并行合并竞速取最快。Future完全没有编排能力。

类比:打开商品详情页需要同时加载「基本信息+库存+价格+评价」,你得逐个等,代码又丑又长。

java 复制代码
ExecutorService executor = Executors.newFixedThreadPool(4);

Future<String> baseInfo = executor.submit(() -> getBaseInfo());
Future<Integer> stock = executor.submit(() -> getStock());
Future<Double> price = executor.submit(() -> getPrice());
Future<List<String>> reviews = executor.submit(() -> getReviews());

// 逐个阻塞------4个任务就写4次get()
String info = baseInfo.get();
Integer s = stock.get();
Double p = price.get();
List<String> r = reviews.get();

// 想串行 A→B?必须先get(),再submit,再get()------毫无优雅可言
编排模式 含义 Future做法
串行依赖 B需要A的结果 get(),再submit()
并行汇聚 等全部完成 逐个get()
竞速 谁快用谁 轮询isDone()

1.3 异常处理原始 --- 挂了就挂了,没有Plan B

Future把任务中的异常延迟到 get() 时才抛出 ,且没有提供异常恢复路径(降级兜底)。

类比:你同时查三个渠道的价格,其中一个挂了,你想要的只是"拿缓存顶上",但Future做不到。

java 复制代码
Future<Double> priceFromCache = executor.submit(() -> queryCache());

try {
    Double cachePrice = priceFromCache.get();
} catch (ExecutionException e) {
    // 只能捕获,无法说"失败了用数据库的价格代替"
    // ❌ 想要的:priceFromCache.exceptionally(e -> queryDB());
}
问题 说明
异常延迟 任务执行时就崩了,你到get()才知道
必须解包装 e.getCause()才能拿到真实异常
无恢复路径 无法提供默认值或降级逻辑

1.4 无法手动完成 --- 你只能看,不能碰

Future的结果完全由任务自身决定,外部无法注入值或异常

类比:第三方支付接口5秒没返回,你想手动标记为"处理中"让前端先展示,Future做不到。

java 复制代码
Future<PayResult> payFuture = executor.submit(() -> {
    return thirdPartyPayAPI.pay(orderId, amount); // 可能10秒才返回
});

try {
    PayResult result = payFuture.get(5, TimeUnit.SECONDS);
} catch (TimeoutException e) {
    // ❌ 想手动给个值:payFuture.complete(new PayResult("PENDING"));
    // Future 没有 complete() 方法!
    payFuture.cancel(true); // 只能粗暴中断
}
能力 Future
手动注入结果
手动注入异常
外部干预 只能 cancel()

小结 :Future是一个被动的、只读的、阻塞的 异步结果容器。它解决了"有没有异步"的问题,但没解决"异步该怎么用"的问题。这正是 CompletableFuture 要解决的问题。


二、CompletableFuture四大解决方案

2.1 回调机制 --- 外卖到了,手机自动通知你

CompletableFuture 提供回调链:任务完成后自动触发,调用线程完全自由。

java 复制代码
// ❌ Future:阻塞等
Future<String> future = executor.submit(() -> "外卖已送达");
String result = future.get(); // 卡死
System.out.println(result);

// ✅ CompletableFuture:注册回调,非阻塞
CompletableFuture.supplyAsync(() -> {
    sleep(1000);
    return "外卖已送达";
}).thenAccept(result -> {           // 回调由ForkJoinPool自动触发
    System.out.println("收到通知:" + result);
});

// 主线程继续做自己的事
System.out.println("看书...");
System.out.println("洗碗...");
维度 Future.get() CompletableFuture.thenAccept()
主线程状态 阻塞 自由
通知方式 你主动去拿 任务完成自动回调
线程利用率

核心 API 速查

方法 用途
thenApply(fn) 转换结果
thenAccept(fn) 消费结果(不返回)
thenRun(fn) 不关心结果,只触发动作
whenComplete(fn) 无论成败都执行

2.2 任务编排 --- 声明式组合,一行搞定

四种编排模式覆盖所有异步协作场景:

模式 API 场景
串行 thenCompose(fn) B 依赖 A 的结果(有返回值)
双任务合并 thenCombine(fn) 两个任务都完成后合并
等待全部 allOf(...) N 个任务全部完成
竞速 anyOf(...) N 个任务谁快用谁
串行依赖 --- 先查用户,再查订单
java 复制代码
CompletableFuture<List<Order>> orders = CompletableFuture
    .supplyAsync(() -> getUser(uid))               // 第1步
    .thenCompose(user ->                            // 第2步依赖上一步
        CompletableFuture.supplyAsync(() -> getOrders(user.getId()))
    );
并行合并 --- 同时查库存 + 价格
java 复制代码
CompletableFuture<Integer> stock = CompletableFuture.supplyAsync(() -> getStock(sku));
CompletableFuture<Double> price = CompletableFuture.supplyAsync(() -> getPrice(sku));

String result = stock.thenCombine(price, (s, p) ->
    "库存:" + s + " 价格:" + p
).join();
竞速取最快 --- Redis / 本地缓存 / DB,谁快用谁
java 复制代码
String data = CompletableFuture.anyOf(
    CompletableFuture.supplyAsync(() -> queryLocalCache(key)),
    CompletableFuture.supplyAsync(() -> queryRedis(key)),
    CompletableFuture.supplyAsync(() -> queryDB(key))
).thenApply(obj -> (String) obj)
 .join();

关键 :编排是声明式的------你描述任务关系,框架调度,代码量与任务数无关。


2.3 异常恢复 --- 挂了也能优雅兜底

三层异常处理,解决 Future 只能被动捕获的痛点:

API 执行时机 能否恢复
exceptionally(fn) 仅异常时 ✅ 返回兜底值
handle(fn) 正常/异常都执行 ✅ 可返回兜底值
whenComplete(fn) 正常/异常都执行 ❌ 类似finally
exceptionally --- 最常用的降级模式
java 复制代码
CompletableFuture<Double> price = CompletableFuture
    .supplyAsync(() -> queryPriceFromRemote()) // 远程可能挂
    .exceptionally(ex -> {
        log.warn("远程失败,用缓存兜底", ex);
        return queryPriceFromCache();          // 返回降级值
    });
// price 一定有值:要么远程,要么缓存
handle --- 统一处理正常/异常
java 复制代码
CompletableFuture<String> result = CompletableFuture
    .supplyAsync(() -> queryFromDB())
    .handle((data, ex) -> {
        if (ex != null) return "不可用";
        return "可用:" + data;
    });
whenComplete --- 类似finally,只记录不干预
java 复制代码
CompletableFuture.supplyAsync(() -> sensitiveOp())
    .whenComplete((data, ex) -> {
        if (ex != null) log.error("失败,记审计日志", ex);
        // 注意:这里不return,异常继续传播
    });

2.4 手动完成 --- 任何时候都可以注入结果

外部线程可通过 complete() / completeExceptionally() 主动控制 Future 的结果。

超时兜底 --- 支付接口5秒没返回,手动标记
java 复制代码
CompletableFuture<String> payResult = new CompletableFuture<>();

// 线程A:真正调支付
new Thread(() -> {
    try {
        String result = thirdPartyPayAPI.pay(orderId, amount);
        payResult.complete(result);
    } catch (Exception e) {
        payResult.completeExceptionally(e);
    }
}).start();

// 线程B:5秒超时检测
new Thread(() -> {
    sleep(5000);
    if (!payResult.isDone()) {
        payResult.complete("PENDING_VERIFY"); // ⭐ 手动完成
    }
}).start();
缓存回填 --- 缓存命中直接返回,不等数据库
java 复制代码
CompletableFuture<User> userFuture = new CompletableFuture<>();

User cached = cache.get(userId);
if (cached != null) {
    userFuture.complete(cached); // ⭐ 直接完成,不查库
} else {
    new Thread(() -> userFuture.complete(userDao.findById(userId))).start();
}
能力 Future CompletableFuture
注入结果 complete(value)
注入异常 completeExceptionally(ex)
手动完成后自动触发回调 无回调概念 ✅ 自动触发

小结CompletableFuture 将异步编程从"主动等待"变为"被动通知",从"手动编排"变为"声明式组合",从"异常崩溃"变为"优雅恢复"。


三、实战技巧

3.1 allOf批量等待 + get(timeout) --- 生产级写法

常见场景:接口收到一批商品编码,逐个刷新缓存,全部完成后返回结果。矛盾在于------单个任务是异步的,但整体结果必须同步等待(API 要等全部完成才能响应)。

java 复制代码
public Map<String, String> batchRefresh(List<String> codes) {
    // 1. 为每个 code 创建异步任务
    List<CompletableFuture<Map.Entry<String, String>>> futures = codes.stream()
        .map(code -> CompletableFuture.supplyAsync(() -> {
            try {
                refreshCache(code);
                return Map.entry(code, "成功");
            } catch (Exception e) {
                return Map.entry(code, "失败:" + e.getMessage());
            }
        }, executor))
        .collect(Collectors.toList());

    // 2. allOf 等待全部完成
    CompletableFuture<Void> allDone = CompletableFuture.allOf(
        futures.toArray(new CompletableFuture[0])
    );

    // 3. 带超时的阻塞等待(生产必备)
    try {
        allDone.get(30, TimeUnit.SECONDS);
    } catch (TimeoutException e) {
        // 超时处理:标记未完成的任务
        futures.forEach(f -> {
            if (!f.isDone()) f.completeExceptionally(new TimeoutException());
        });
    } catch (Exception e) {
        throw new RuntimeException("批量刷新失败", e);
    }

    // 4. 收集结果(此时不会阻塞)
    return futures.stream()
        .map(f -> {
            try {
                return f.get();
            } catch (Exception e) {
                return Map.entry("unknown", "失败");
            }
        })
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
join() vs get() 选择建议
方法 异常类型 try-catch 超时
get() checked Exception ✅ 必须 ✅ 支持
get(timeout, unit) checked + TimeoutException ✅ 必须 ✅ 自带
join() unchecked CompletionException ❌ 不强制 ❌ 不支持

建议生产环境用 get(timeout) ,避免某个任务卡死导致整个接口挂掉。测试/演示用 join() 更简洁。


3.2 常用API速查表

创建异步任务
API 用途
CompletableFuture.supplyAsync(fn) 有返回值的异步任务
CompletableFuture.runAsync(fn) 无返回值的异步任务
CompletableFuture.completedFuture(val) 直接返回已完成的结果
new CompletableFuture<>() 创建空壳,等待外部 complete
结果转换与消费
API 用途
thenApply(fn) 转换结果(T → U
thenAccept(fn) 消费结果(T → void
thenRun(fn) 不关心结果,只触发动作
任务编排
API 用途
thenCompose(fn) 串行依赖(T → CompletableFuture<U>
thenCombine(other, fn) 两任务合并((T, U) → V
allOf(...) 等待全部完成
anyOf(...) 取最快完成的
异常处理
API 用途
exceptionally(fn) 异常时返回兜底值
handle(fn) 正常/异常统一处理
whenComplete(fn) 类似 finally
手动控制
API 用途
complete(val) 手动完成(注入结果)
completeExceptionally(ex) 手动完成(注入异常)
cancel(mayInterrupt) 取消任务

一句话总结Future 让你有了异步的能力,CompletableFuture 让你真正用好了异步。

相关推荐
鹅城剑仙1 小时前
Java CompletableFuture 异步编程完全指南
java
2601_961875241 小时前
法考备考计划表|学习计划|资料已整理
java·开发语言·学习·eclipse·tomcat·c#·hibernate
青春:一叶知秋1 小时前
【Python】python基本语法和使用
开发语言·python
SilentSamsara1 小时前
向量数据库实战:Chroma/Milvus/Qdrant 选型与语义搜索应用
开发语言·数据库·人工智能·python·青少年编程·milvus
重生之我是Java开发战士1 小时前
【Java SE】多线程(三):单例模式,阻塞队列,线程池与定时器
java·javascript·单例模式
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题 第115题】【并发篇】第15题:说一下悲观锁和乐观锁的区别?
java·开发语言·面试
lijgvnns2 小时前
个人AI编程工具的vibe coding实践:从爬虫到导出Excel的全流程
开发语言·javascript·ecmascript
心之伊始2 小时前
Spring Boot Actuator + Micrometer 实战:自定义业务指标并接入 Prometheus 观测接口耗时
java·spring boot·prometheus·actuator·micrometer
Full Stack Developme2 小时前
Spring Integration 教程
java·后端·spring