深入理解Java并发编程:CompletableFuture 完全指南
前言
在Java 8之前,我们使用Future来获取异步计算的结果,但Future的局限性非常明显——它只是一个结果的容器,我们无法对其结果进行链式处理、组合多个异步任务,或者优雅地处理异常。直到Java 8引入了CompletableFuture,这一切才发生了改变。
CompletableFuture是Java并发编程中的一个里程碑式的API,它不仅实现了Future接口,还实现了CompletionStage接口,提供了丰富的链式调用、组合、异常处理等能力,让我们能够以声明式的方式编写复杂的异步逻辑。
本文将从基础概念到高级用法,带你全面掌握CompletableFuture的核心知识。
一、CompletableFuture 概述
1.1 什么是 CompletableFuture
CompletableFuture是Java 8引入的一个类,它同时实现了Future和CompletionStage两个接口:
- Future接口:提供了获取异步计算结果的基本能力
- CompletionStage接口:提供了大约50种方法,用于链式组合多个异步任务
CompletableFuture的核心思想是:将异步计算的结果看作一个"阶段"(Stage),每个阶段完成后可以触发下一个阶段,形成一条异步流水线。
1.2 为什么需要 CompletableFuture
传统Future的痛点:
- 无法手动完成:Future只能由执行任务的线程完成,调用方无法主动设置结果
- 无法链式调用:获取结果后无法直接传递给下一个异步任务
- 无法组合多个Future:多个异步任务之间的依赖关系难以表达
- 异常处理困难:异步任务中的异常难以优雅捕获和处理
- 阻塞式获取结果:get()方法会阻塞,直到结果返回
CompletableFuture完美解决了这些问题。
二、核心 API 详解
2.1 创建 CompletableFuture
方式一:使用 completedFuture 创建已完成的 Future
// 创建一个已经完成的CompletableFuture,值为"Hello"CompletableFuture<String>future=CompletableFuture.completedFuture("Hello");这在测试或者需要立即返回结果的场景中非常有用。
方式二:使用 supplyAsync 异步执行有返回值的任务
CompletableFuture<String>future=CompletableFuture.supplyAsync(()->{// 模拟耗时操作try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}return"异步计算结果";});supplyAsync接受一个Supplier函数式接口,有返回值。
方式三:使用 runAsync 异步执行无返回值的任务
CompletableFuture<Void>future=CompletableFuture.runAsync(()->{// 模拟耗时操作,无返回值try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("异步任务执行完成");});runAsync接受一个Runnable函数式接口,无返回值。
方式四:手动创建并完成
CompletableFuture<String>future=newCompletableFuture<>();// 在某个线程中手动完成newThread(()->{try{Thread.sleep(1000);future.complete("手动设置的结果");}catch(InterruptedExceptione){future.completeExceptionally(e);}}).start();这是CompletableFuture最强大的特性之一——你可以在任何时间、任何线程中手动完成它。
2.2 线程池的选择
默认情况下,supplyAsync和runAsync会使用**ForkJoinPool.commonPool()**作为线程池。这个公共线程池的大小默认为CPU核心数-1。
但我们也可以指定自己的线程池:
ExecutorServiceexecutor=Executors.newFixedThreadPool(10);CompletableFuture<String>future=CompletableFuture.supplyAsync(()->{return"使用自定义线程池";},executor);最佳实践:对于IO密集型任务,建议使用自定义线程池,避免耗尽公共线程池。
2.3 链式调用:处理异步结果
CompletableFuture最强大的地方在于它的链式调用能力。
thenApply:转换结果
CompletableFuture<String>future=CompletableFuture.supplyAsync(()->"Hello").thenApply(s->s+" World").thenApply(String::toUpperCase);// 输出: HELLO WORLDSystem.out.println(future.get());thenApply接受一个Function函数式接口,将上一阶段的结果进行转换,返回新的结果。
thenAccept:消费结果
CompletableFuture.supplyAsync(()->"Hello").thenApply(s->s+" World").thenAccept(System.out::println);// 输出: Hello WorldthenAccept接受一个Consumer函数式接口,消费上一阶段的结果,无返回值。
thenRun:执行后续操作
CompletableFuture.supplyAsync(()->{System.out.println("执行任务");return"result";}).thenRun(()->{System.out.println("任务完成后的收尾工作");});thenRun接受一个Runnable,不关心上一阶段的结果,只是在完成后执行某个动作。
异步版本的链式调用
上面的方法都有对应的异步版本:thenApplyAsync、thenAcceptAsync、thenRunAsync。
它们的区别是:
- 同步版本(不带Async):使用与上一阶段相同的线程执行
- 异步版本(带Async):重新提交到线程池中执行
CompletableFuture.supplyAsync(()->{System.out.println("supplyAsync: "+Thread.currentThread().getName());return"Hello";}).thenApply(s->{System.out.println("thenApply: "+Thread.currentThread().getName());returns+" World";}).thenApplyAsync(s->{System.out.println("thenApplyAsync: "+Thread.currentThread().getName());returns.toUpperCase();});2.4 组合多个 CompletableFuture
thenCompose:扁平化组合
当你有一个返回CompletableFuture的函数时,使用thenApply会导致嵌套的CompletableFuture:
// 错误示范:嵌套的CompletableFutureCompletableFuture<CompletableFuture<String>>future=CompletableFuture.supplyAsync(()->"Hello").thenApply(s->getUserAsync(s));这时候应该使用thenCompose,它类似于Stream的flatMap:
// 正确示范:扁平化组合CompletableFuture<String>future=CompletableFuture.supplyAsync(()->"Hello").thenCompose(s->getUserAsync(s));thenApply vs thenCompose 的区别:
thenApply:map操作,返回普通值thenCompose:flatMap操作,返回CompletableFuture
thenCombine:组合两个Future的结果
CompletableFuture<String>future1=CompletableFuture.supplyAsync(()->"Hello");CompletableFuture<String>future2=CompletableFuture.supplyAsync(()->"World");CompletableFuture<String>combined=future1.thenCombine(future2,(s1,s2)->s1+" "+s2);// 输出: Hello WorldSystem.out.println(combined.get());thenCombine等待两个Future都完成后,用BiFunction组合它们的结果。
allOf:等待所有Future完成
CompletableFuture<String>future1=CompletableFuture.supplyAsync(()->"结果1");CompletableFuture<String>future2=CompletableFuture.supplyAsync(()->"结果2");CompletableFuture<String>future3=CompletableFuture.supplyAsync(()->"结果3");CompletableFuture<Void>allFutures=CompletableFuture.allOf(future1,future2,future3);// 等待所有任务完成allFutures.get();// 然后分别获取结果System.out.println(future1.get());System.out.println(future2.get());System.out.println(future3.get());注意:allOf返回的是CompletableFuture<Void>,它不返回所有结果的集合,只是表示所有任务都完成了。
如果需要收集所有结果,可以这样做:
List<CompletableFuture<String>>futures=Arrays.asList(future1,future2,future3);CompletableFuture<List<String>>allResults=CompletableFuture.allOf(futures.toArray(newCompletableFuture[0])).thenApply(v->futures.stream().map(CompletableFuture::join).collect(Collectors.toList()));anyOf:任意一个Future完成即可
CompletableFuture<String>future1=CompletableFuture.supplyAsync(()->{sleep(100);return"快速结果";});CompletableFuture<String>future2=CompletableFuture.supplyAsync(()->{sleep(1000);return"慢速结果";});CompletableFuture<Object>anyFuture=CompletableFuture.anyOf(future1,future2);// 输出: 快速结果(谁先完成返回谁)System.out.println(anyFuture.get());2.5 异常处理
CompletableFuture提供了三种异常处理方式。
exceptionally:捕获异常并返回默认值
CompletableFuture<String>future=CompletableFuture.supplyAsync(()->{thrownewRuntimeException("计算出错了");}).exceptionally(ex->{System.out.println("捕获异常: "+ex.getMessage());return"默认值";});// 输出: 默认值System.out.println(future.get());exceptionally类似于catch块,当上面的任何阶段抛出异常时,都会进入这里。
handle:无论成功还是异常都执行
CompletableFuture<String>future=CompletableFuture.supplyAsync(()->{thrownewRuntimeException("出错了");}).handle((result,ex)->{if(ex!=null){System.out.println("发生异常: "+ex.getMessage());return"恢复值";}returnresult;});handle类似于try-catch-finally中的finally,无论成功还是失败都会执行。
whenComplete:只做副作用,不改变结果
CompletableFuture<String>future=CompletableFuture.supplyAsync(()->{return"正常结果";}).whenComplete((result,ex)->{if(ex!=null){System.out.println("记录异常日志: "+ex.getMessage());}else{System.out.println("记录成功日志,结果是: "+result);}});whenComplete不改变结果,只是在完成时执行一些副作用操作(如日志记录)。
三、实战案例
3.1 案例一:电商商品详情页聚合
假设我们要开发一个电商商品详情页,需要并行获取以下信息:
- 商品基本信息
- 商品价格
- 商品库存
- 商品评价
使用CompletableFuture可以轻松实现并行获取:
publicProductDetailgetProductDetail(LongproductId){// 并行获取各个信息CompletableFuture<ProductInfo>productInfoFuture=CompletableFuture.supplyAsync(()->productService.getProductInfo(productId));CompletableFuture<PriceInfo>priceInfoFuture=CompletableFuture.supplyAsync(()->priceService.getPriceInfo(productId));CompletableFuture<StockInfo>stockInfoFuture=CompletableFuture.supplyAsync(()->stockService.getStockInfo(productId));CompletableFuture<ReviewInfo>reviewInfoFuture=CompletableFuture.supplyAsync(()->reviewService.getReviewInfo(productId));// 等待所有任务完成并组装结果returnCompletableFuture.allOf(productInfoFuture,priceInfoFuture,stockInfoFuture,reviewInfoFuture).thenApply(v->{ProductDetaildetail=newProductDetail();detail.setProductInfo(productInfoFuture.join());detail.setPriceInfo(priceInfoFuture.join());detail.setStockInfo(stockInfoFuture.join());detail.setReviewInfo(reviewInfoFuture.join());returndetail;}).join();}这样,四个接口调用是并行执行的,总耗时取决于最慢的那个,而不是四个的总和。
3.2 案例二:多级依赖的异步流水线
假设我们有一个复杂的业务流程:
- 先查询用户信息
- 根据用户信息查询用户的订单
- 根据订单查询订单详情
- 最后组装完整的用户订单视图
publicUserOrderViewgetUserOrderView(LonguserId){returnCompletableFuture.supplyAsync(()->userService.getUser(userId)).thenCompose(user->orderService.getOrdersAsync(user.getId())).thenCompose(orders->orderDetailService.getDetailsAsync(orders)).thenApply(details->{UserOrderViewview=newUserOrderView();view.setOrderDetails(details);returnview;}).exceptionally(ex->{log.error("获取用户订单视图失败",ex);returnnewUserOrderView();// 返回空视图}).join();}使用thenCompose可以优雅地表达多级依赖关系。
3.3 案例三:超时控制
在实际项目中,我们经常需要给异步任务设置超时时间,避免无限等待。
public<T>CompletableFuture<T>withTimeout(CompletableFuture<T>future,longtimeout,TimeUnitunit){// 创建一个超时用的CompletableFutureCompletableFuture<T>timeoutFuture=newCompletableFuture<>();// 定时任务,超时后完成timeoutFuture并抛出异常ScheduledExecutorServicescheduler=Executors.newScheduledThreadPool(1);scheduler.schedule(()->timeoutFuture.completeExceptionally(newTimeoutException("任务超时")),timeout,unit);// 谁先完成用谁的结果returnCompletableFuture.anyOf(future,timeoutFuture).thenApply(o->(T)o).whenComplete((result,ex)->scheduler.shutdown());}使用方法:
CompletableFuture<String>future=CompletableFuture.supplyAsync(()->{// 模拟耗时操作sleep(5000);return"结果";});// 设置3秒超时Stringresult=withTimeout(future,3,TimeUnit.SECONDS).exceptionally(ex->"超时默认值").get();四、最佳实践与注意事项
4.1 get() vs join()
两者都是获取结果的方法,区别在于:
| 方法 | 异常类型 | 是否受检异常 |
|---|---|---|
| get() | ExecutionException | 是,需要try-catch |
| join() | CompletionException | 否,运行时异常 |
推荐在流式调用中使用join(),因为它不需要强制捕获异常。
4.2 合理选择线程池
不要所有场景都用默认的ForkJoinPool:
- CPU密集型任务:可以使用默认的ForkJoinPool.commonPool()
- IO密集型任务:必须使用自定义线程池,线程数可以设置得大一些
- 业务隔离:不同业务使用不同的线程池,避免互相影响
4.3 异常处理的最佳位置
建议在整个链路的最后统一处理异常:
// 推荐:最后统一处理异常CompletableFuture.supplyAsync(()->step1()).thenApply(r->step2(r)).thenApply(r->step3(r)).exceptionally(ex->{// 统一处理所有异常log.error("任务执行失败",ex);returndefaultValue;});而不是每个步骤都处理异常,那样会让代码变得混乱。
4.4 避免 CompletableFuture 泄露
如果创建了CompletableFuture但忘记complete它,会导致等待它的线程永远阻塞:
// 危险:如果某些分支没有complete,会导致永久阻塞CompletableFuture<String>future=newCompletableFuture<>();if(condition){future.complete("success");}// else分支没有complete!建议:总是确保所有分支都能complete,或者设置超时机制。
4.5 谨慎使用 thenApply 等同步方法
thenApply、thenAccept、thenRun这些不带Async的方法,会在上一个任务的线程中执行。如果上一个任务是在IO线程中执行的,而你的thenApply里又有耗时操作,可能会阻塞IO线程。
建议:如果后续操作比较耗时,使用带Async的版本。
五、总结
CompletableFuture是Java并发编程的一大利器,让我们回顾一下它的核心能力:
- 灵活的创建方式:supplyAsync、runAsync、手动complete
- 强大的链式调用:thenApply、thenAccept、thenRun
- 丰富的组合能力:thenCompose、thenCombine、allOf、anyOf
- 优雅的异常处理:exceptionally、handle、whenComplete
掌握了CompletableFuture,你就能写出更加优雅、高效的异步代码。但也要注意合理使用线程池、做好异常处理、避免常见的陷阱。
在实际项目中,CompletableFuture常用于:
- 接口聚合,并行调用多个服务
- 异步化处理,提升响应速度
- 复杂的业务流程编排
- 超时控制、降级容错
希望这篇文章能帮助你深入理解CompletableFuture,在并发编程的道路上更进一步!
参考资料:
- Java官方文档:CompletableFuture
- Java官方文档:CompletionStage
- 《Java并发编程实战》
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、关注!有任何问题也可以在评论区留言讨论。
