JDK1.8中的 CompletableFuture
为我们提供了异步函数式编程,CompletableFuture
提供了非常强大的Future
的扩展功能,可以帮助我们简化异步编程的复杂性,提供了函数式编程的能力,可以通过回调的方式处理计算结果,并且提供了转换和组合CompletableFuture
的方法。
一个例子回顾 Future
因为CompletableFuture
实现了Future
接口,先来回顾Future
吧。
Future
是Java5新加的一个接口,它提供了一种异步并行计算的功能。
如果主线程需要执行一个很耗时的计算任务,就可以通过future
把这个任务放到异步线程中执行。
主线程继续处理其他任务,处理完成后,再通过Future
获取计算结果。
来看个简单例子吧,假设有两个任务,一个查询用户基本信息,一个是查询用户勋章信息。
如下:
1 | public class UserInfoService { |
接下来,来演示下,在主线程中是如何使用Future
来进行异步调用的。
1 | public class FutureTest { |
运行结果:
1 | 总共用时806ms |
如果不使用Future进行并行异步调用,而是在主线程串行进行的话,耗时大约为300+500+300 = 1100 ms。
可以发现,future+线程池异步配合,提高了程序的执行效率。
但是Future对于结果的获取,不是很友好,只能通过阻塞或者轮询的方式得到任务的结果。
Future.get()
就是阻塞调用,在线程获取结果之前get
方法会一直阻塞。- Future提供了一个
isDone
方法,可以在程序中轮询这个方法查询执行结果。
阻塞的方式和异步编程的设计理念相违背,而轮询的方式会耗费无谓的CPU资源。
因此,JDK8设计出CompletableFuture
。
CompletableFuture
提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方。
一个例子走进CompletableFuture
还是基于以上Future
的例子,改用CompletableFuture
来实现。
1 | public class FutureTest { |
可以发现,使用CompletableFuture
,代码简洁了很多。
CompletableFuture
的supplyAsync
方法,提供了异步执行的功能,线程池也不用单独创建了。
实际上,它CompletableFuture
使用了默认线程池是ForkJoinPool.commonPool
。
CompletableFuture
提供了几十种方法辅助异步任务场景。这些方法包括创建异步任务、任务异步回调、多个任务组合处理等方面。
CompletableFuture使用场景
创建异步任务
CompletableFuture
创建异步任务,一般有supplyAsync
和runAsync
两个方法
创建异步任务
supplyAsync
执行CompletableFuture
任务,支持返回值runAsync
执行CompletableFuture
任务,没有返回值。
supplyAsync方法
1 | // 使用默认内置线程池ForkJoinPool.commonPool(),根据supplier构建执行任务 |
runAsync方法
1 | // 使用默认内置线程池ForkJoinPool.commonPool(),根据runnable构建执行任务 |
实例代码如下:
1 | public class FutureTest { |
任务异步回调
thenRun/thenRunAsync
1 | public CompletableFuture<Void> thenRun(Runnable action); |
CompletableFuture
的thenRun
方法,通俗点讲就是,做完第一个任务后,再做第二个任务。
某个任务执行完成后,执行回调方法;但是前后两个任务没有参数传递,第二个任务也没有返回值
1 | public class FutureThenRunTest { |
thenRun
和thenRunAsync
有什么区别呢?可以看下源码:
1 | private static final Executor asyncPool = useCommonPool ? |
如果执行第一个任务的时候,传入了一个自定义线程池:
- 调用
thenRun
方法执行第二个任务时,则第二个任务和第一个任务是共用同一个线程池。 - 调用
thenRunAsync
执行第二个任务时,则第一个任务使用的是你自己传入的线程池,第二个任务使用的是ForkJoin线程池
TIPS:后面介绍的thenAccept
和thenAcceptAsync
,thenApply
和thenApplyAsync
等,它们之间的区别也是这个。
thenAccept/thenAcceptAsync
CompletableFuture
的thenAccept
方法表示,第一个任务执行完成后,执行第二个回调方法任务,会将该任务的执行结果,作为入参,传递到回调方法中,但是回调方法是没有返回值
的。
1 | public class FutureThenAcceptTest { |
thenApply/thenApplyAsync
CompletableFuture
的thenApply
方法表示,第一个任务执行完成后,执行第二个回调方法任务,会将该任务的执行结果,作为入参,传递到回调方法中,并且回调方法是有返回值
的。
1 | public class FutureThenApplyTest { |
exceptionally
CompletableFuture
的exceptionally
方法表示,某个任务执行异常时,执行的回调方法,并且有抛出异常作为参数,传递到回调方法。
1 | public class FutureExceptionTest { |
whenComplete
CompletableFuture
的whenComplete
方法表示,某个任务执行完成后,执行的回调方法,无返回值;
并且whenComplete
方法返回的CompletableFuture
的result
是上个任务的结果。
1 | public class FutureWhenTest { |
handle
CompletableFuture
的handle
方法表示,某个任务执行完成后,执行回调方法,并且是有返回值的;并且handle
方法返回的CompletableFuture
的result
是回调方法执行的结果。
1 | public class FutureHandlerTest { |
多个任务组合处理
AND组合关系
thenCombine
/ thenAcceptBoth
/ runAfterBoth
都表示:将两个CompletableFuture
组合起来,只有这两个都正常执行完了,才会执行某个任务。
区别在于:
thenCombine
:会将两个任务的执行结果作为方法入参,传递到指定方法中,且有返回值。thenAcceptBoth
: 会将两个任务的执行结果作为方法入参,传递到指定方法中,且无返回值。runAfterBoth
不会把执行结果当做方法入参,且没有返回值。
1 | public class ThenCombineTest { |
OR 组合的关系
applyToEither
/ acceptEither
/ runAfterEither
都表示:将两个CompletableFuture
组合起来,只要其中一个执行完了,就会执行某个任务。
区别在于:
applyToEither
:会将已经执行完成的任务,作为方法入参,传递到指定方法中,且有返回值。acceptEither
: 会将已经执行完成的任务,作为方法入参,传递到指定方法中,且无返回值。runAfterEither
:不会把执行结果当做方法入参,且没有返回值。
1 | public class AcceptEitherTest { |
AllOf
所有任务都执行完成后,才执行 allOf
返回的CompletableFuture
。
如果任意一个任务异常,allOf
的CompletableFuture
,执行get
方法,会抛出异常。
1 | public class allOfFutureTest { |
AnyOf
任意一个任务执行完,就执行anyOf
返回的CompletableFuture
。
如果执行的任务异常,anyOf
的CompletableFuture
,执行get
方法,会抛出异常。
1 | public class AnyOfFutureTest { |
thenCompose
thenCompose
方法会在某个任务执行完成后,将该任务的执行结果,作为方法入参,去执行指定的方法。
该方法会返回一个新的CompletableFuture
实例
- 如果该
CompletableFuture
实例的result
不为null,则返回一个基于该result
新的CompletableFuture
实例。 - 如果该
CompletableFuture
实例为null
,然后就执行这个新任务。
1 | public class ThenComposeTest { |
CompletableFuture使用有哪些注意点
CompletableFuture
使异步编程更加便利的、代码更加优雅的同时,也要关注下它的一些注意点。
Future需要获取返回值,才能获取异常信息
1 | ExecutorService executorService = new ThreadPoolExecutor(5, 10, 5L, |
Future
需要获取返回值,才能获取到异常信息。
如果不加 get()
/join()
方法,看不到异常信息。
使用的时候,注意一下,考虑是否加try...catch...
或者使用exceptionally
方法。
CompletableFuture的get()方法是阻塞的
CompletableFuture
的get()
方法是阻塞的,如果使用它来获取异步调用的返回值,需要添加超时时间
1 | // 反例 |
默认线程池的注意点
CompletableFuture
代码中又使用了默认的线程池,处理的线程个数是电脑CPU核数-1。
在大量请求过来的时候,处理逻辑复杂的话,响应会很慢。
一般建议使用自定义线程池,优化线程池配置参数。
自定义线程池时,注意饱和策略
CompletableFuture
的get()
方法是阻塞的,一般建议使用future.get(3, TimeUnit.SECONDS)
,并且一般建议使用自定义线程池。
但是如果线程池拒绝策略是DiscardPolicy
或者DiscardOldestPolicy
,当线程池饱和时,会直接丢弃任务,不会抛弃异常。
因此建议,CompletableFuture
线程池策略最好使用AbortPolicy
,然后耗时的异步线程,做好线程池隔离。