稀土掘金技术社区 01月28日
CompletableFuture还能这么玩
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入探讨了Java异步编程的利器CompletableFuture,从基础概念到高级特性,进行了详尽的讲解。文章首先回顾了传统Future的局限性,引出CompletableFuture的优势,如异步回调、任务编排等。接着,详细介绍了CompletableFuture的多种创建方式,包括supplyAsync和runAsync的区别,以及自定义线程池的最佳实践。此外,文章还阐述了如何进行任务的取消和超时处理,并深入剖析了链式调用的艺术,如thenApply、thenAccept、thenRun以及异步转换、组合操作等。最后,文章还介绍了CompletableFuture的异常处理技巧,包括exceptionally、handle和whenComplete的使用场景,并通过实际案例,展示了如何利用CompletableFuture构建高效的异步应用。

🚀 CompletableFuture是Future的升级版,解决了传统Future的局限性,支持异步回调和任务编排等高级功能,使得异步编程更加灵活高效。

🛠️ 创建异步任务时,supplyAsync适用于需要返回结果的场景,而runAsync则适用于不需要返回结果的场景。同时,自定义线程池可以更好地管理和控制并发任务,避免资源竞争和性能瓶颈。

🔗 CompletableFuture的链式调用通过thenApply、thenAccept、thenRun等方法,实现任务的串行或并行处理。thenCompose用于一个异步操作依赖另一个操作的结果,而thenCombine则用于组合两个独立的异步操作。

🛡️ 异常处理是异步编程的关键环节,exceptionally提供应急预案,handle能同时处理正常结果和异常情况,whenComplete则用于在任务完成时执行一些附加操作,确保程序的健壮性。

原创 一只叫煤球的猫 2025-01-25 09:03 重庆

点击关注公众号,“技术干货” 及时达!

点击关注公众号,“技术干货” 及时达!

前言

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

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

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

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

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

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

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

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


正文

回顾基础

还记得刚接触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("我只是默默地执行,不给你返回值");
});

选择建议:

自定义线程池的正确姿势

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

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

// 错误示范:这是一匹脱缰的野马!
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));

小贴士:

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

如果超时了,要么给个替代方案(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("生产线工作完成,开始打扫");
});

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

异步转换:thenApplyAsync的使用场景

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

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

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

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

CompletableFuture<String> getUserEmail(String userId) {
return CompletableFuture.supplyAsync(() -> "user@example.com");
}
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> "userId")
.thenCompose(userId -> getUserEmail(userId)); // 基于第一个结果去获取邮箱
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);
});

使用建议:

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

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);
});
}

使用建议

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 + " - 已处理";
});

小贴士

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

结尾

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

那么,敬请期待!

点击关注公众号,“技术干货” 及时达!

阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

CompletableFuture 异步编程 Java多线程 任务编排 异常处理
相关文章