前言
当我决定写这篇关于 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("我只是默默地执行,不给你返回值");
});
选择建议:
- 需要返回值的时候:用
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
whenComplete
和 handle
看起来很像,但用途不同,看代码和注释就明白了:
// 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
:当你只关心异常情况,并且只需要提供一个替代值时
希望永远也用不到这些异常处理的技巧,谁说不是呢~
结尾
第一篇主打基础操作,后面第二篇上重菜:任务编排、实战技巧、性能优化等等,当然还有喜闻乐见的虚拟线程。
那么,敬请期待!