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

一、Future 类的作用

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

  1. 获取异步任务结果 :通过 get() 方法阻塞等待任务完成并获取结果

  2. 检查任务状态isDone() 判断是否完成,isCancelled() 判断是否被取消

  3. 取消任务cancel(boolean mayInterruptIfRunning) 尝试取消未完成的任务

  4. 实现"将来式"模式 :调用线程提交任务后立即返回一个"凭证"(Future),稍后再凭此获取结果

典型使用场景

java 复制代码
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)

工作流程

java 复制代码
// 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. 非阻塞式异步编排

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

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

2. 强大的任务编排能力

java 复制代码
// 场景:同时查询用户信息和订单信息,最后合并结果
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()

java 复制代码
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:降级处理(推荐生产环境使用)

java 复制代码
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:统一处理(记录日志 + 保留异常)

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

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

java 复制代码
// 不做异常处理,异常会向后传播
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() 降级:

java 复制代码
// 错误做法:一个失败,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()

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

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

java 复制代码
// 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 中使用阻塞操作而不自定义线程池。生产环境必须为不同类型任务配置独立线程池,并做好监控(如线程池活跃数、队列大小)。

相关推荐
uzong2 小时前
软件工程师应该尽量改掉的坏习惯
后端
蒸蒸yyyyzwd2 小时前
cpp os 计网学习笔记
笔记·学习
爆米花byh2 小时前
在RockyLinux9环境的Doris单机版安装
linux·数据库·database
南 阳2 小时前
Python从入门到精通day34
开发语言·python
前路不黑暗@2 小时前
Java项目:Java脚手架项目的统一模块的封装(四)
java·开发语言·spring boot·笔记·学习·spring cloud·maven
2401_848009722 小时前
Redis进阶学习
数据库·redis·学习·缓存
喵呜嘻嘻嘻2 小时前
Gurobi求解器参数
java·数据结构·算法
消失的旧时光-19432 小时前
第二十四课:从 Java 后端到系统架构——后端能力体系的最终总结
java·开发语言·系统架构