从零起步学习并发编程 || 第九章:Future 类详解及CompletableFuture 类在项目实战中的应用

一、Future 类的作用

Future 是 Java 1.5 引入的接口,代表异步计算的结果,核心作用是:

  1. 获取异步任务结果 :通过 get() 方法阻塞等待任务完成并获取结果
  2. 检查任务状态isDone() 判断是否完成,isCancelled() 判断是否被取消
  3. 取消任务cancel(boolean mayInterruptIfRunning) 尝试取消未完成的任务
  4. 实现"将来式"模式:调用线程提交任务后立即返回一个"凭证"(Future),稍后再凭此获取结果

典型使用场景

ini 复制代码
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<Integer> future = executor.submit(() -> {
    // 模拟耗时计算
    Thread.sleep(2000);
    return 42;
});

// 主线程可以继续做其他事情...
System.out.println("任务已提交,继续执行其他操作");

// 需要结果时再获取(会阻塞直到任务完成)
Integer result = future.get(); // 阻塞等待
System.out.println("计算结果: " + result);

⚠️ Future 的局限性get() 是阻塞式调用,无法链式编排多个异步任务,异常处理不够灵活。


二、Callable 和 Future 的关系

它们是生产者-消费者的关系:

对比项 Callable Future
角色 任务生产者:定义可返回结果的异步任务 结果消费者:持有任务结果的"凭证"
核心方法 V call():执行任务并返回结果,可抛出异常 V get():获取结果;boolean cancel():取消任务
与Runnable区别 Runnable 无返回值、不能抛出受检异常;Callable 可返回结果且支持异常 Future 专为配合 Callable 设计(也支持 Runnable)

工作流程

ini 复制代码
// 1. 定义任务(Callable - 生产结果)
Callable<String> task = () -> {
    Thread.sleep(1000);
    return "Hello Future";
};

// 2. 提交任务到线程池
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(task); // submit返回Future

// 3. 获取结果(Future - 消费结果)
String result = future.get(); // 阻塞直到call()执行完成

💡 关键理解Callable 负责产生 结果,Future 负责获取 结果,二者通过 ExecutorService.submit() 关联


三、CompletableFuture 类的作用

CompletableFuture 是 Java 8 引入的增强版 Future,解决了传统 Future 的痛点,核心优势:

1. 非阻塞式异步编排

rust 复制代码
// 传统Future:必须阻塞等待
future.get(); // 阻塞!

// CompletableFuture:回调式处理
CompletableFuture.supplyAsync(() -> "Hello")
    .thenApply(s -> s + " World")      // 链式转换
    .thenAccept(System.out::println);  // 结果消费(不阻塞主线程)

2. 强大的任务编排能力

scss 复制代码
// 场景:同时查询用户信息和订单信息,最后合并结果
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> getUserById(1));
CompletableFuture<Order> orderFuture = CompletableFuture.supplyAsync(() -> getOrdersByUserId(1));

// thenCombine:两个异步任务都完成后合并结果
CompletableFuture<String> result = userFuture.thenCombine(orderFuture, (user, order) -> 
    "用户" + user.getName() + "有" + order.getItems().size() + "个订单"
);

result.thenAccept(System.out::println); // 异步打印,不阻塞

3. 实际应用场景

  • 并行聚合 :首页加载需调用10+个微服务(用户信息、商品推荐、优惠券等),用 CompletableFuture.allOf() 并行执行
  • 流水线处理 :订单支付 → 更新库存 → 发送通知,用 thenApply/thenCompose 串行编排
  • 超时控制orTimeout(3, TimeUnit.SECONDS) 避免长时间阻塞
  • 异常处理exceptionally()/handle() 统一处理链路中的异常

4. 与 Future 对比

特性 Future CompletableFuture
获取结果 阻塞式 get() 非阻塞回调(thenApply等)
任务编排 不支持 支持链式调用、组合、并行
异常处理 需手动 try-catch 内置 exceptionally()/handle()
主动完成 仅任务自身完成 可通过 complete() 主动设置结果

四 . 如何使用CompletableFuture实现一个任务需要依赖另外两个任务执行完之后再执行,怎么设计?

任务依赖设计:A、B 完成后执行 C

✅ 推荐方案:CompletableFuture.allOf() + thenApply()

ini 复制代码
ExecutorService customPool = Executors.newFixedThreadPool(4);

// 任务A:获取用户信息(模拟耗时1秒)
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> {
    System.out.println("【A】开始获取用户信息 - " + Thread.currentThread().getName());
    Thread.sleep(1000);
    return new User(1L, "张三");
}, customPool);

// 任务B:获取订单列表(模拟耗时1.5秒)
CompletableFuture<List<Order>> orderFuture = CompletableFuture.supplyAsync(() -> {
    System.out.println("【B】开始获取订单列表 - " + Thread.currentThread().getName());
    Thread.sleep(1500);
    return Arrays.asList(new Order(101), new Order(102));
}, customPool);

// 任务C:依赖A和B的结果生成报表
CompletableFuture<Report> reportFuture = CompletableFuture.allOf(userFuture, orderFuture)
    .thenApply(v -> {
        // allOf 不返回结果,需手动获取
        User user = userFuture.join();          // join() 不会抛出受检异常
        List<Order> orders = orderFuture.join();
        System.out.println("【C】开始生成报表 - " + Thread.currentThread().getName());
        return new Report(user, orders);
    });

// 获取最终结果(生产环境建议设置超时)
Report report = reportFuture.get(5, TimeUnit.SECONDS);
System.out.println("报表生成完成: " + report);

🔁 其他方案对比

方案 适用场景 优点 缺点
thenCombine() 仅依赖两个任务 语法简洁,自动传递结果 仅支持两个任务组合
allOf() + join() 依赖N个任务 可组合任意数量任务 需手动调用 join() 获取结果
thenCompose() 嵌套 串行依赖(A→B→C) 处理链式依赖 不适合并行场景

💡 最佳实践 :超过 2 个任务并行依赖时,优先用 allOf();仅 2 个任务可用 thenCombine() 简化代码。

五 . 使用 CompletableFuture,有一个任务失败,如何处理异常?

CompletableFuture 提供三种异常处理策略,按场景选择:

🛡️ 策略1:降级处理(推荐生产环境使用)

javascript 复制代码
CompletableFuture<String> task = CompletableFuture.supplyAsync(() -> {
    if (Math.random() > 0.5) throw new RuntimeException("服务异常");
    return "成功结果";
}, customPool);

// 任务失败时返回默认值
CompletableFuture<String> withFallback = task.exceptionally(ex -> {
    System.err.println("【降级】捕获异常: " + ex.getMessage());
    return "默认结果"; // 降级返回
});

System.out.println(withFallback.join()); // 永远不会抛出异常

🔍 策略2:统一处理(记录日志 + 保留异常)

kotlin 复制代码
task.handle((result, ex) -> {
    if (ex != null) {
        System.err.println("【统一处理】任务失败: " + ex.getMessage());
        // 可记录监控日志、发送告警
        return "处理失败";
    }
    return "处理成功: " + result;
});

⚠️ 策略3:传播异常(快速失败)

rust 复制代码
// 不做异常处理,异常会向后传播
CompletableFuture<String> chained = task
    .thenApply(s -> s + " → 步骤2")
    .thenApply(s -> s + " → 步骤3");

try {
    chained.join();
} catch (CompletionException e) {
    System.err.println("【传播】最终捕获异常: " + e.getCause().getMessage());
}

📌 异常处理关键点

方法 是否消费异常 返回类型 适用场景
exceptionally() ✅ 消费 与原任务相同 降级、默认值
handle() ✅ 消费 自定义类型 统一日志/监控
whenComplete() ❌ 不消费 原类型 仅记录日志,异常继续传播
不处理 ❌ 不消费 - 快速失败,由最终调用方处理

⚠️ 重要allOf()任意一个任务失败,整个组合立即失败 。若需"部分失败仍继续",需对每个子任务单独做 exceptionally() 降级:

ini 复制代码
// 错误做法:一个失败,allOf 立即失败
CompletableFuture.allOf(failTask, successTask).join(); 

// 正确做法:每个任务单独降级
CompletableFuture<String> safeA = taskA.exceptionally(ex -> "A降级");
CompletableFuture<String> safeB = taskB.exceptionally(ex -> "B降级");
CompletableFuture.allOf(safeA, safeB).join(); // 即使A失败,B仍会执行

六 . 为什么必须自定义线程池?

❌ 默认线程池的风险(ForkJoinPool.commonPool()

scss 复制代码
// 危险!使用默认线程池
CompletableFuture.supplyAsync(() -> doSomething()); 
风险点 说明 后果
阻塞操作导致线程饥饿 默认池是工作窃取线程池,设计用于 CPU 密集型任务。若执行 I/O 阻塞(如 HTTP 调用、DB 查询),线程被挂起无法处理其他任务 线程池耗尽,整个应用卡死
与并行流共享资源 Stream.parallel() 也使用 commonPool 业务异步任务与数据处理互相争抢线程
无法监控和调优 线程数由系统决定(通常 = CPU 核心数 - 1) 无法根据业务量动态调整
拒绝策略不可控 队列无界,极端情况 OOM 系统稳定性差

✅ 正确做法:按任务类型分离线程池

scss 复制代码
// CPU 密集型(计算、加密等)
ExecutorService cpuPool = new ThreadPoolExecutor(
    Runtime.getRuntime().availableProcessors(),
    Runtime.getRuntime().availableProcessors(),
    60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(100),
    new ThreadFactoryBuilder().setNameFormat("cpu-pool-%d").build()
);

// I/O 密集型(HTTP、DB、文件读写)
ExecutorService ioPool = new ThreadPoolExecutor(
    20,   // 核心线程数(可远大于CPU核心数)
    50,   // 最大线程数
    60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000),
    new ThreadFactoryBuilder().setNameFormat("io-pool-%d").build(),
    new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝时由调用线程执行,避免丢弃
);

// 使用示例
CompletableFuture.supplyAsync(() -> computeHash(data), cpuPool);   // 计算任务
CompletableFuture.supplyAsync(() -> callRemoteApi(), ioPool);      // 网络请求

📊 线程池配置建议

任务类型 核心线程数 队列类型 拒绝策略 说明
CPU 密集型 = CPU 核心数 有界队列 AbortPolicy 避免过多线程上下文切换
I/O 密集型 2~5 × CPU 核心数 有界队列 CallerRunsPolicy 允许更多线程等待 I/O
混合型 按比例拆分 分离队列 自定义 建议拆分为两个独立线程池

💡 黄金法则永远不要在 CompletableFuture 中使用阻塞操作而不自定义线程池。生产环境必须为不同类型任务配置独立线程池,并做好监控(如线程池活跃数、队列大小)。

相关推荐
devlei3 小时前
从源码泄露看AI Agent未来:深度对比Claude Code原生实现与OpenClaw开源方案
android·前端·后端
Accerlator4 小时前
2026 年 4 月 1 日电话面试
面试·职场和发展
努力的小郑5 小时前
Canal 不难,难的是用好:从接入到治理
后端·mysql·性能优化
Victor3566 小时前
MongoDB(87)如何使用GridFS?
后端
Victor3566 小时前
MongoDB(88)如何进行数据迁移?
后端
安审若无6 小时前
运维知识框架
运维·服务器
小红的布丁6 小时前
单线程 Redis 的高性能之道
redis·后端
GetcharZp6 小时前
Go 语言只能写后端?这款 2D 游戏引擎刷新你的认知!
后端
宁瑶琴7 小时前
COBOL语言的云计算
开发语言·后端·golang
普通网友8 小时前
阿里云国际版服务器,真的是学生党的性价比之选吗?
后端·python·阿里云·flask·云计算