CompletableFuture还能这么玩

2,793 阅读10分钟

前言

当我决定写这篇关于 CompletableFuture 的文章时,脑海中浮现出无数个曾经被异步编程折磨得死去活来的瞬间。

所以我希望能够用通俗且有趣的方式,帮列位看官逐步掌握这个Java异步编程的终极武器:CompletableFuture

同时这篇文章,会尽可能的将知识点切成碎片化,不用看到长文就头痛。

坦白说,这篇文章确实有点长!一口气读完还是有点费劲。所以,我决定把它拆分为两篇:

第一篇,也就是本文,主打基础和进阶,深入浅出。

第二篇,重点在高级特性上,比如多任务编排,还会围绕实战和性能优化展开讲讲,这是传送门

无论你是Java新手还是资深开发,相信都能在这里找到值得学习的干货。

耐心看完,你一定有所收获。

雷军 GIF 动图.gif

正文

回顾基础

还记得刚接触Java多线程的时候,有多懵圈吗?Thread、Runnable、Callable...这些概念像脱缰的野马一样在脑海中狂奔。

后来,我们认识了Future,以为终于找到了异步编程的救星,结果发现...

这家伙好像也不太靠谱?

Future接口的局限性

Future接口就像是一个"只能查询、不能改变"的“未来”。你投递了一个任务,然后就只能无奈地等在那里问:"完成了吗?完成了吗?"(通过isDone())。要么就干脆死等着(get())。

Future<String> future = executor.submit(() -> {
    Thread.sleep(1000);
    return "我是结果";
});

// 只能在这傻等
String result = future.get(); // 被迫阻塞

这种方式有多不优雅?就像你点了外卖,但是:

  • 无法告诉外卖员:"送到了给我打电话"
  • 没法和朋友说:"我的外卖到了请你吃"
  • 更无法设置规则:"如果超时了就取消订单"

CompletableFuture是什么

这时候,CompletableFuture闪亮登场!它就像是Future接口的升级版,不仅能完成Future的所有功能,还自带"异步回调"、"任务编排"等高级技能,彻底解决了传统Future的局限性。

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    return "我是结果";
}).thenApply(result -> {
    return "处理一下结果:" + result;
}).thenAccept(finalResult -> {
    System.out.println("最终结果:" + finalResult);
});

看到没?这就像是给外卖配上了现代化的配送系统:

  • 可以设置送达后的自动通知(回调机制)
  • 可以预设各种状态的处理方案(异常处理、条件判断)
  • 甚至可以和其他订单组合起来统一处理(任务编排)

为什么要使用CompletableFuture

说到这里,你可能会问:"Future不也能用吗?为什么非要用CompletableFuture?"

让我们来看个真实场景:假设你要做一个商品详情页,需要同时调用:

  • 商品基本信息
  • 库存信息
  • 促销信息
  • 评价信息

用传统的Future

Future<ProductInfo> productFuture = executor.submit(() -> getProductInfo());
Future<Stock> stockFuture = executor.submit(() -> getStock());
Future<Promotion> promotionFuture = executor.submit(() -> getPromotion());
Future<Comments> commentsFuture = executor.submit(() -> getComments());

// 然后就是一堆get()的等待...痛苦
ProductInfo product = productFuture.get();
Stock stock = stockFuture.get();
// 继续等...

CompletableFuture

CompletableFuture<ProductInfo> productFuture = CompletableFuture.supplyAsync(() -> getProductInfo());
CompletableFuture<Stock> stockFuture = CompletableFuture.supplyAsync(() -> getStock());
CompletableFuture<Promotion> promotionFuture = CompletableFuture.supplyAsync(() -> getPromotion());
CompletableFuture<Comments> commentsFuture = CompletableFuture.supplyAsync(() -> getComments());

CompletableFuture.allOf(productFuture, stockFuture, promotionFuture, commentsFuture)
    .thenAccept(v -> {
        // 所有数据都准备好了,开始组装页面
        buildPage(productFuture.join(), stockFuture.join(), 
                 promotionFuture.join(), commentsFuture.join());
    });

看出区别了吗?CompletableFuture 就像是给你的代码配备了一个小管家:

  • 不用你盯着看是否完成
  • 自动通知你结果已准备好
  • 可以设定各种后续处理方案
  • 多个任务的编排也变得异常简单

所以说,如果你还在用传统的Future,那真的是在给自己找麻烦。现代化的异步编程,CompletableFuture 才是正确的打开方式!

2. 创建异步任务的花式方法

这个话题让我想起了点外卖时选择支付方式的场景 —— 支付宝还是微信?选择困难症又犯了!

supplyAsync vs runAsync的选择

这两个方法就像双胞胎兄弟,长得像但性格完全不同:

// supplyAsync:我做事靠谱,一定给你返回点什么
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
    return "我是有结果的异步任务";
});

// runAsync:我比较佛系,不想给你返回任何东西
CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
    System.out.println("我只是默默地执行,不给你返回值");
});

选择建议:

  • 需要返回值的时候:用supplyAsync,就像点外卖必须要等待送餐小哥送来美食
  • 不需要返回值的时候:用runAsync,就像发朋友圈,发完就完事了,不需要等待结果

自定义线程池的正确姿势

默认的线程池好比是共享单车,小黄、小蓝、小绿,谁都可以用,但高峰期可能要等。

而自定义线程池就像是私家车,只要调校的足够好,想怎么开就怎么开!

// 错误示范:这是一匹脱缰的野马!
ExecutorService wrongPool = Executors.newFixedThreadPool(10);

// 正确示范:这才是精心调教过的千里马
ThreadPoolExecutor rightPool = new ThreadPoolExecutor(
    5,                      // 核心线程数(正式员工)
    10,                     // 最大线程数(含临时工)
    60L,                    // 空闲线程存活时间
    TimeUnit.SECONDS,       // 时间单位
    new LinkedBlockingQueue<>(100),  // 工作队列(候客区)
    new ThreadFactoryBuilder().setNameFormat("async-pool-%d").build(),  // 线程工厂(员工登记处)
    new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略(客满时的处理方案)
);

// 使用自定义线程池
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    return "我是通过专属线程池执行的任务";
}, rightPool);

异步任务的取消和超时处理

就像等外卖的时候,超过预期时间就想取消订单(还是建议耐心等一等,你永远不知道送餐小哥正在做什么伟大的事情)。CompletableFuture也支持这种"任性"的操作:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    // 模拟一个耗时操作
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        // 被中断时的处理
        return "我被中断了!";
    }
    return "正常完成";
});

// 设置超时
try {
    String result = future.get(3, TimeUnit.SECONDS);
} catch (TimeoutException e) {
    future.cancel(true);  // 超时就取消任务
    System.out.println("等太久了,不等了!");
}

// 更优雅的超时处理
future.completeOnTimeout("默认值", 3, TimeUnit.SECONDS)
      .thenAccept(result -> System.out.println("最终结果:" + result));

// 或者配合orTimeout使用
future.orTimeout(3, TimeUnit.SECONDS)  // 超时就抛异常
      .exceptionally(ex -> "超时默认值")
      .thenAccept(result -> System.out.println("最终结果:" + result));

小贴士:

  • 取消任务时,cancel(true)表示允许中断正在执行的任务,cancel(false)表示仅取消还未执行的任务
  • completeOnTimeout比直接使用get更优雅,因为它不会阻塞
  • orTimeout适合那些超时必须处理的场景,比如支付操作

或者换个例子,异步任务的超时控制就像餐厅的叫号系统——不能让顾客无限等待,要给出一个合理的预期时间。

如果超时了,要么给个替代方案(completeOnTimeout),要么直接请顾客重新取号(orTimeout)。

3. 链式调用的艺术

CompletableFuture的链式调用就像是一条生产流水线,原材料经过层层加工,最终变成成品。

thenApply、thenAccept、thenRun的区别

这三个方法像是流水线上的三种工人,各司其职:

CompletableFuture.supplyAsync(() -> "Hello")
    .thenApply(s -> {
        // 我是加工工人,负责把材料加工后返回新成品
        return s + " World";
    })
    .thenAccept(result -> {
        // 我是检验工人,只负责验收,不返回东西
        System.out.println("收到结果: " + result);
    })
    .thenRun(() -> {
        // 我是打扫工人,不关心之前的结果,只负责收尾工作
        System.out.println("生产线工作完成,开始打扫");
    });

通过这个例子就能看明白各自的用途:

  • thenApply:当你需要转换结果并继续传递时使用
  • thenAccept:当你只需要处理结果,不需要返回值时使用
  • thenRun:当你只需要执行一个操作,不需要使用结果时使用

异步转换:thenApplyAsync的使用场景

有时候,转换操作本身也很耗时,这时就需要用到thenApplyAsync

CompletableFuture.supplyAsync(() -> {
    // 模拟获取用户信息
    return "用户基础信息";
}).thenApplyAsync(info -> {
    // 耗时的处理操作,在新的线程中执行
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    return info + " + 附加信息";
}, customExecutor);  // 可以指定自己的线程池

组合多个异步操作:thenCompose vs thenCombine

这两个方法其实就是两种不同的协作模式,一个串行,一个并行:

  • thenCompose - 串行操作(一个接一个):
CompletableFuture<String> getUserEmail(String userId) {
    return CompletableFuture.supplyAsync(() -> "user@example.com");
}

CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> "userId")
    .thenCompose(userId -> getUserEmail(userId));  // 基于第一个结果去获取邮箱
  • thenCombine - 并行操作(两个一起做):
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "价格信息");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "库存信息");

CompletableFuture<String> result = future1.thenCombine(future2, (price, stock) -> {
    // 同时处理价格和库存信息
    return String.format("价格: %s, 库存: %s", price, stock);
});

使用建议:

  • 当一个异步操作依赖另一个操作的结果时,使用thenCompose
  • 当两个异步操作相互独立,但最终结果需要组合时,使用thenCombine

在结合一个实际案例,比如商品详情页的数据聚合

public CompletableFuture<ProductDetails> getProductDetails(String productId) {
    CompletableFuture<Product> productFuture = getProduct(productId);
    
    return productFuture.thenCompose(product -> {
        // 基于商品信息获取促销信息
        CompletableFuture<Promotion> promotionFuture = getPromotion(product.getCategory());
        // 同时获取评论信息
        CompletableFuture<Reviews> reviewsFuture = getReviews(productId);
        
        // 组合促销和评论信息
        return promotionFuture.thenCombine(reviewsFuture, (promotion, reviews) -> {
            return new ProductDetails(product, promotion, reviews);
        });
    });
}

4. 异常处理的技巧

异常处理其实就是安全气囊 —— 不是每天都用得到,但关键时刻能救命。

应急的exceptionally

exceptionally可以理解成一个应急预案,当主流程出现问题时,它会提供一个替代方案:

CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> {
        if (Math.random() < 0.5) {
            throw new RuntimeException("服务暂时不可用");
        }
        return "正常返回的数据";
    })
    .exceptionally(throwable -> {
        // 记录异常日志
        log.error("操作失败", throwable);
        // 返回默认值
        return "服务异常,返回默认数据";
    });

也可以区分异常类型,进行针对性的处理:

CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> callExternalService())
    .exceptionally(throwable -> {
        if (throwable.getCause() instanceof TimeoutException) {
            return "服务超时,返回缓存数据";
        } else if (throwable.getCause() instanceof IllegalArgumentException) {
            return "参数异常,返回空结果";
        }
        return "其他异常,返回默认值";
    });

两全其美的handle

handle方法比exceptionally更强大,在于它能同时处理正常结果和异常情况:

CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> {
        if (Math.random() < 0.5) {
            throw new RuntimeException("模拟服务异常");
        }
        return "原始数据";
    })
    .handle((result, throwable) -> {
        if (throwable != null) {
            log.error("处理异常", throwable);
            return "发生异常,返回备用数据";
        }
        return result + " - 正常处理完成";
    });

举个比较常见的例子,处理订单:

public CompletableFuture<OrderResult> processOrder(Order order) {
    return CompletableFuture
        .supplyAsync(() -> validateOrder(order))
        .thenApply(validOrder -> processPayment(validOrder))
        .handle((paymentResult, throwable) -> {
            if (throwable != null) {
                // 支付过程中出现异常
                if (throwable.getCause() instanceof PaymentDeclinedException) {
                    return new OrderResult(OrderStatus.PAYMENT_FAILED, "支付被拒绝");
                } else if (throwable.getCause() instanceof SystemException) {
                    // 触发补偿机制
                    compensateOrder(order);
                    return new OrderResult(OrderStatus.SYSTEM_ERROR, "系统异常");
                }
                return new OrderResult(OrderStatus.UNKNOWN_ERROR, "未知错误");
            }
            // 正常完成支付
            return new OrderResult(OrderStatus.SUCCESS, paymentResult);
        });
}

使用建议

  • 优先使用 exceptionally
  • 需要在处理结果的同时执行一些附加操作(如记录日志、发送指标等),使用handle更合适

whenComplete

whenCompletehandle 看起来很像,但用途不同,看代码和注释就明白了:

// whenComplete:只是旁观者,不能修改结果
CompletableFuture<String> future1 = CompletableFuture
    .supplyAsync(() -> "原始数据")
    .whenComplete((result, throwable) -> {
        // 只能查看结果,无法修改
        if (throwable != null) {
            log.error("发生异常", throwable);
        } else {
            log.info("处理完成: {}", result);
        }
    });

// handle:既是参与者又是修改者
CompletableFuture<String> future2 = CompletableFuture
    .supplyAsync(() -> "原始数据")
    .handle((result, throwable) -> {
        // 可以根据结果或异常,返回新的值
        if (throwable != null) {
            return "异常情况下的替代数据";
        }
        return result + " - 已处理";
    });

小贴士

  • 使用whenComplete:当你只需要记录日志或执行一些清理工作,不需要改变结果时
  • 使用handle:当你需要在异常发生时返回备用值,或者需要转换成正常的结果时
  • 使用exceptionally:当你只关心异常情况,并且只需要提供一个替代值时

希望永远也用不到这些异常处理的技巧,谁说不是呢~

结尾

第一篇主打基础操作,后面第二篇上重菜:任务编排、实战技巧、性能优化等等,当然还有喜闻乐见的虚拟线程。

那么,敬请期待!