JUC工具类:CompletableFuture介绍

前言

​ 一些业务场景我们需要使用多线程异步执行任务,加快任务执行速度。JDK5新增了Future接口,用于描述一个异步计算的结果。虽然 Future 以及相关使用方法提供了异步执行任务的能力,但是对于结果的获取却是很不方便,我们必须使用Future.get()的方式阻塞调用线程,或者使用轮询方式判断 Future.isDone 任务是否结束,再获取结果。

​ 与此同时,Future无法解决多个异步任务需要相互依赖的场景,简单点说就是,主线程需要等待子线程任务执行完毕之后在进行执行,这个时候你可能想到了CountDownLatch,没错确实可以解决。

​ 通过CompletableFuture可以很轻松的实现CountDownLatch的功能。比如可以实现 :任务1执行完了再执行任务2,甚至任务1执行的结果,作为任务2的入参数等等强大功能。

使用

1、常用4种创建方式

1)supplyAsync(Supplier supplier)(支持返回值)

//使用默认内置线程池ForkJoinPool.commonPool(),根据supplier构建执行任务

2)supplyAsync(Supplier supplier,Executor executor)

//自定义线程,根据supplier构建执行任务

3)runAsync(Runnable runnable)(没有返回值)

//使用默认内置线程池ForkJoinPool.commonPool(),根据runnable构建执行任务

4)runAsync(Runnable runnable,Executor executor)

//自定义线程,根据runnable构建执行任务

2、结果获取的4种方式

1)public T get()

​ 在Future中就已经提供了,继承了原有获取结果方式

2)public T get(long timeout, TimeUnit unit)

​ 也在Future中就已经提供了,在上述的情况下添加超时处理,如果在指定时间内未获取结果将抛出超时异常

3)public T getNow(T valueIfAbsent)

​ 立即获取结果不阻塞,结果计算已完成将返回结果或计算过程中的异常,如果未计算完成将返回设定的valueIfAbsent值

4)public T join()

​ **用于等待异步任务完成并获取结果的方法。**如果异步任务已经完成,则该方法会立即返回任务的执行结果;如果异步任务尚未完成,则该方法会阻塞当前线程,直到任务执行完成并返回结果为止。

3、异步回调方法

1)thenRun/thenRunAsync

​ **做完第一个任务后,再做第二个任务,第二个任务也没有返回值。**如果你执行第一个任务的时候,传入了一个自定义线程池:

  • 调用thenRun方法执行第二个任务时,则第二个任务和第一个任务是共用同一个线程池。
  • 调用thenRunAsync执行第二个任务时,则第一个任务使用的是你自己传入的线程池,第二个任务使用的是ForkJoin线程池。

说明:

​ 后面介绍的thenAcceptthenAcceptAsyncthenApplythenApplyAsync等,它们之间的区别也是这个。

2)thenAccept/thenAcceptAsync

第一个任务执行完成后,执行第二个回调方法任务,会将该任务的执行结果,作为入参 ,传递到回调方法中,但是回调方法是没有返回值的。

3)thenApply/thenApplyAsync

​ 表示第一个任务执行完成后,执行第二个回调方法任务,会将该任务的执行结果,作为入参,传递到回调方法中,并且回调方法是有返回值的。

4、异常回调

​ 当CompletableFuture的任务不论是正常完成还是出现异常它都会调用 「whenComplete」 这回调函数。

  • 「正常完成」 :whenComplete返回结果和上级任务一致;
  • 「出现异常」 :whenComplete返回结果为null,异常为上级任务的异常;

即调用get()时,正常完成时就获取到结果,出现异常时就会抛出异常,需要你处理该异常。

示例

@Test  
public void testWhenCompleteExceptionally() throws ExecutionException, InterruptedException {  
    CompletableFuture<Double> future = CompletableFuture.supplyAsync(() -> {  
        if (Math.random() < 0.5) {  
            throw new RuntimeException("出错了");  
        }  
        System.out.println("正常结束");  
        return 0.11;  

    }).whenComplete((aDouble, throwable) -> {  
        if (aDouble == null) {  
            System.out.println("whenComplete aDouble is null");  
        } else {  
            System.out.println("whenComplete aDouble is " + aDouble);  
        }  
        if (throwable == null) {  
            System.out.println("whenComplete throwable is null");  
        } else {  
            System.out.println("whenComplete throwable is " + throwable.getMessage());  
        }  
    }).exceptionally((throwable) -> {  
        System.out.println("exceptionally中异常:" + throwable.getMessage());  
        return 0.0;  
    });  

    System.out.println("最终返回的结果 = " + future.get());  
}  

当出现异常时,exceptionally中会捕获该异常,给出默认返回值0.0。

5、多任务组合回调

1)AND组合关系

thenCombine / thenAcceptBoth / runAfterBoth都表示:「当任务一和任务二都完成再执行任务三」

区别在于:

  • 「runAfterBoth」 不会把执行结果当做方法入参,且没有返回值
  • 「thenAcceptBoth」 : 会将两个任务的执行结果作为方法入参,传递到指定方法中,且无返回值
  • 「thenCombine」 :会将两个任务的执行结果作为方法入参,传递到指定方法中,且有返回值

2)OR组合关系

applyToEither / acceptEither / runAfterEither 都表示:「两个任务,只要有一个任务完成,就执行任务三」

区别在于:

  • 「runAfterEither」 :不会把执行结果当做方法入参,且没有返回值
  • 「acceptEither」 : 会将已经执行完成的任务,作为方法入参,传递到指定方法中,且无返回值
  • 「applyToEither」 :会将已经执行完成的任务,作为方法入参,传递到指定方法中,且有返回值

3)多任务组合

  • 「allOf」 :等待所有任务完成
  • 「anyOf」 :只要有一个任务完成

注意点

1、Future需要获取返回值,才能获取异常信息

2、CompletableFuture的get()方法是阻塞的

CompletableFutureget()方法是阻塞的,如果使用它来获取异步调用的返回值,需要添加超时时间。

3、不建议使用默认线程池

CompletableFuture代码中又使用了默认的 「ForkJoin线程池」 ,处理的线程个数是电脑 「CPU核数-1」 。在大量请求过来的时候,处理逻辑复杂的话,响应会很慢。一般建议使用自定义线程池,优化线程池配置参数。

4、自定义线程池时,需要注意饱和策略

CompletableFuture的get()方法是阻塞的,我们一般建议使用future.get(5, TimeUnit.SECONDS)。并且一般建议使用自定义线程池。

小结

​ 但是如果线程池拒绝策略是DiscardPolicy或者DiscardOldestPolicy,当线程池饱和时,会直接丢弃任务,不会抛出异常。因此建议,CompletableFuture线程池策略最好使用AbortPolicy,然后耗时的异步线程,做好线程池隔离。

原理分析

CompletableFuture类结构

//CompletableFuture类结构
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
    volatile Object result;       // Either the result or boxed AltResult
    volatile Completion stack;    // Top of Treiber stack of dependent actions
    ...
}
//stack属性栈结构
abstract static class Completion extends ForkJoinTask<Void>
    implements Runnable, AsynchronousCompletionTask {
    volatile Completion next;      // Treiber stack link
    ...
}

**result:**存储CompletableFuture的返回,或者存储异常对象AltResult。
**stack:**是CompletableFuture.Completion对象,表示操作数栈栈顶,在进行CompletableFuture链式调用的过程中,所有链式调用的CompletableFuture任务都会被压入该stack中,在任务调用的过程按后进先出的顺序出栈执行完所有任务。

CompletableFuture回调原理

​ CompletableFuture在执行任务时会创建出新的CompletableFuture对象,使用新对象执行任务并获取结果使用CAS写入到result属性。如果任务未执行将压入栈顶,再重新尝试任务执行,在CompletableFuture其他方法的调用也都大同小异。

CompletableFuture异步原理

​ CompletableFuture异步调用则要使用Async结尾的方法执行任务。主要是通过将任务压入栈顶后,使用tryFire方法进行异步处理,如果任务未被执行,则会通过postFire方法,由线程池中的线程进行任务执行,任务执行结果再使用CAS将结果返回到result。其他线程即可得知任务是否被执行过,如果当前现场判断当前任务为被执行,则调用postComplete()执行完成任务。

总结

CompletableFuture通过异步回调的方式,解决了开发过程中异步调用获取结果的难点。开发者只需接触到CompletableFuture对象,以及CompletableFuture任务的执行结果,无需设计具体异步回调的实现,并可通过自定义线程池进一步优化任务的异步调用。

你可能感兴趣的:(多线程与线程并发,java)