CompleteFuture详解

CompletableFuture

1、CompletableFuture是什么?

CompletableFuture是Java 8中引入的一个类,它实现了Future接口,用于表示异步计算的结果。与Future不同,CompletableFuture提供了更丰富的方法来处理计算过程中的异常、组合多个计算任务以及链式调用等。

CompletableFuture的定义:
在这里插入图片描述
CompletableFuture 实现了两个接口(如上图所示):Future、CompletionStage。 Future 表示异步计算的结果,CompletionStage用于表示异步执行过程中的一个步骤(Stage),这个步骤可能是由另外一个CompletionStage触发的,随着当前步骤的完成,也可能会触发其他一系列CompletionStage的执行。从而我们可以根据实际业务对这些步骤进行多样化的编排组合,CompletionStage接口正是定义了这样的能力,我们可以通过其提供的thenAppy、thenCompose等函数式编程方法来组合编排这些步骤。

2、为什么使用CompletableFuture?

2.1 Future的局限性

在Java8之前我们一般通过Future实现异步,Future用于表示异步计算的结果。

  • 只能通过get()阻塞或者isDone()轮询的方式获取结果。
  • 不支持设置回调方法,Java 8之前若要设置回调一般会使用guava的 ListenableFuture,回调的引入又会导致臭名昭著的回调地狱。
  • 不能将多个Future合并在一起。 假设你有多种不同的Future, 你想在它们全部并行完成后然后运行某个函数,Future很难独立完成这一需要。
  • 没有异常处理。 Future提供的方法中没有专门的API应对异常处理,还需要开发者自己手动异常处理。

下面将举例来说明,我们通过Future、ListenableFuture来实现异步回调:

例子:替换文章中的敏感词。

  1. 读取文章,step1
  2. 读取敏感词,step2
  3. 替换敏感词,step3

其中step3依赖step1、step2的结果。

public static void main(String[] args) {
    // 创建线程池
    ExecutorService executor = Executors.newFixedThreadPool(5);
    // 对ExecutorService进行装饰成为ListeningExecutorService,Future ==>ListenableFuture,使其具有监听任务执行状态的功能,并能进行回调
    ListeningExecutorService guavaExecutor = MoreExecutors.listeningDecorator(executor);

    // 读取文章,step1
    ListenableFuture<String> future1 = guavaExecutor.submit(CommonUtils::readArticle);
    // 读取敏感词,step2
    ListenableFuture<Map<String,String>> future2 = guavaExecutor.submit(CommonUtils::readWords);
    ListenableFuture<List<Object>> future1And2 = Futures.allAsList(future1, future2);
    // 执行step1、step2的回调
    Futures.addCallback(future1And2, new FutureCallback<List<Object>>() {
        @Override
        public void onSuccess(List<Object> result) {
            // step1、step2 执行完成,并获取结果
            String article = (String) result.get(0);
            Map<String,String> words = (Map<String, String>) result.get(1);
            // 替换敏感词,step3,并将结果回调打印出来
            ListenableFuture<String> future3 = guavaExecutor.submit(() -> CommonUtils.replace(article, words));
            Futures.addCallback(future3, new FutureCallback<String>() {
                @Override
                public void onSuccess(String result) {
                    CommonUtils.printTheadLog(result);
                }
                @Override
                public void onFailure(Throwable t) {}
            }, guavaExecutor);
        }
        @Override
        public void onFailure(Throwable t) {}
    }, guavaExecutor);
}

2.2 CompletableFuture的优势

CompletableFuture相对于Future具有以下的优势:

  • 为快速创建、链接依赖和结合多个Future提供了大量的便利方法。
  • 提供了适用于各种开发场景的回调函数,它还提供了非常全面的异常处理支持。
  • 它无疑衔接和亲和Java 8 提供的Lambda表达式和Stream - API。
  • 真正意义上的异步编程,把异步编程和函数式编程、响应式编程多种高阶编程思维集于一身,设计上更优雅。

下面将举例来说明,我们通过CompletableFuture来实现异步回调:

public static void main(String[] args) {
    // 创建线程池
    ExecutorService executor = Executors.newFixedThreadPool(5);
    // 读取文章,step1
    CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
        String article = CommonUtils.readArticle();
        CommonUtils.printTheadLog(article);
        return article;
    }, executor);
    // 读取敏感词,step2
    CompletableFuture<Map<String,String>> cf2 = CompletableFuture.supplyAsync(() -> {
        Map<String, String> words = CommonUtils.readWords();
        CommonUtils.printTheadLog(words.toString());
        return words;
    }, executor);

    // 替换敏感词,step3,并将结果回调打印出来
    cf1.thenCombine(cf2, (article, words) -> CommonUtils.replace(article, words)).thenAccept(result3 -> {
        CommonUtils.printTheadLog(result3);
    });
}

显然,CompletableFuture 的实现更为简洁,可读性更好。

3、如何使用CompletableFuture?

下面讲解一下CompletableFuture如何使用,使用Completable Future 也是构建依赖树的过程。一个CompletableFuture的完成会触发另外一系列 依赖它的CompletableFuture的执行:
在这里插入图片描述
如上图所示,这里描绘的是一个业务接口的流程,其中包括CF1/CF2/CF3/CF4/CF5共5个步骤,并描绘了这些步骤之间的依赖关系,每个步骤可以是一次RPC调 用、一次数据库操作或者是一次本地方法调用等,在使用CompletableFuture进行 异步化编程时,图中的每个步骤都会产生一个CompletableFuture对象,最终结果 也会用一个CompletableFuture来进行表示。

根据CompletableFuture依赖数量,可以分为以下几类:零依赖、一元依赖、二元 依赖和多元依赖。

3.1 零依赖:CompletableFuture的创建

我们先看下如何不依赖其他CompletableFuture来创建新的CompletableFuture:
在这里插入图片描述
如上图红色链路所示,接口接收到请求后,首先发起两个异步调用CF1、CF2,主要有三种方式:

 ExecutorService executor = Executors.newFixedThreadPool(5);
//1、使用runAsync或supplyAsync发起异步调用
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
    return "result1";
}, executor);

//2、CompletableFuture.completedFuture() 直接创建一个已完成状态的CompletableFuture
CompletableFuture<String> cf2 = CompletableFuture.completedFuture("result2");

//3、先初始化一个未完成的CompletableFuture,然后通过complete()、completeExceptionally(),完成该CompletableFuture
CompletableFuture<String> cf = new CompletableFuture<>();
cf.complete("result3");

3.2 一元依赖:依赖一个CF

在这里插入图片描述
如上图红色链路所示,CF3,CF5分别依赖于CF1和CF2,这种对于单个CompletableFuture 的依赖可以通过thenApply、thenAccept、thenCompose 等方法来实现,代码如下所示:

CompletableFuture<String> cf3 = cf1.thenApply(result1 -> {
    //result1 为CF1的结果
    System.out.println(result1);
    return "result3";
});

CompletableFuture<String> cf5 = cf2.thenApply(result2 -> {
    //result2 为CF2的结果
    System.out.println(result2);
    return "result5";
});

3.3 二元依赖:依赖两个CF

在这里插入图片描述
如上图红色链路所示,CF4同时依赖于两个CF1和CF2,这种二元依赖可以通过 thenCombine 等回调来实现,如下代码所示:

CompletableFuture<String> cf4 = cf1.thenCombine(cf2, (result1, result2) -> {
    //result1 和result2分别为cf1和cf2的结果
    System.out.println(result1);
    System.out.println(result2);
    return "result4";
});

3.4 多元依赖:依赖多个CF

在这里插入图片描述
如上图红色链路所示,整个流程的结束依赖于三个步骤CF3、CF4、CF5,这种多 元依赖可以通过allOf或anyOf方法来实现,区别是当需要多个依赖全部完成时使用allOf,当多个依赖中的任意一个完成即可时使用anyOf,如下代码所示:

CompletableFuture<Void> cf6 = CompletableFuture.allOf(cf3, cf4, cf5);
CompletableFuture<String> result = cf6.thenApply(v -> {
    String result3 = cf3.join();
    String result4 = cf4.join();
    String result5 = cf5.join();

    // 根据result3、result4、result5组装最终result;
    StringJoiner res = new StringJoiner("-").add(result3).add(result4).add(result5);
    System.out.println(res);
    return "result6";
});

4、CompletableFuture 原理

CompletableFuture 中包含两个字段:result 和stack。result 用于存储当前CF的结果,stack(Completion)表示当前CF完成后需要触发的依赖动作,去触发依赖它的CF的计算,依赖动作可以有多个(表示有多个依赖它的CF),以栈(Treiber stack)的形式存储,stack表示栈顶元素。

CF的基本结构:
在这里插入图片描述
这种方式类似“观察者模式”,依赖动作(Dependency Action)都封装在一个单独 Completion 子类中。下面是Completion类关系结构图。CompletableFuture中 的每个方法都对应了图中的一个Completion的子类,Completion本身是观察者的基类。

  • UniCompletion 继承了 Completion,是一元依赖的基类,例如thenApply 的实现类UniApply就继承自UniCompletion。
  • BiCompletion 继承了UniCompletion,是二元依赖的基类,同时也是多元依赖的基类。例如thenCombine的实现类BiRelay就继承自BiCompletion。
    在这里插入图片描述

设计思想:

按照类似“观察者模式”的设计思想,原理分析可以从“观察者”和“被观察者” 两个方面着手。

一元依赖:

由于回调种类多,但结构差异不大,所以这里单以一元依赖中的 thenApply 为例,不再枚举全部回调类型。如下图所示:
在这里插入图片描述
被观察者:

  1. 每个CompletableFuture 都可以被看作一个被观察者,其内部有一个 Completion类型的链表成员变量stack,用来存储注册到其中的所有观察者。当被观察者执行完成后会弹栈stack属性,依次通知注册到其中的观察者。上面例子中步骤fn2就是作为观察者被封装在UniApply中。

  2. 被观察者CF中的result属性,用来存储当前CF的执行结果数据。上面例子就是存储CF1的执行结果。

观察者:

CompletableFuture 支持很多回调方法,例如thenAccept、thenApply、exceptionally 等,这些方法接收一个函数类型的参数f,生成一个Completion类型的对象(即观察者),并将入参函数f赋值给Completion的成员变量fn,然后检查当前 CF是否已处于完成状态(即result != null),如果已完成直接触发fn,否则将观察者 Completion 加入到CF的观察者链stack中,再次尝试触发,如果被观察者未执行完则其执行完毕之后通知触发。 1. 观察者中的dep属性:指向其对应的CompletableFuture,在上面的例子中 dep 指向CF2。 2. 观察者中的src属性:指向其依赖的CompletableFuture,在上面的例子中 src 指向CF1。 3. 观察者Completion中的fn属性:用来存储具体的等待被回调的函数。这里需 要注意的是不同的回调方法(thenAccept、thenApply、exceptionally等)接收的函数类型也不同,即fn的类型有很多种,在上面的例子中fn指向fn2。

二元依赖:

我们以thenCombine为例来说明二元依赖:
在这里插入图片描述
thenCombine 操作表示依赖两个CompletableFuture。其观察者实现类为BiApply, 如上图所示,BiApply通过src和snd两个属性关联被依赖的两个CF,fn属性的 类型为BiFunction。与单个依赖不同的是,在依赖的CF未完成的情况下,thenCombine会尝试将BiApply压入这两个被依赖的CF的栈中,每个被依赖的CF完 成时都会尝试触发观察者BiApply,BiApply会检查两个依赖是否都完成,如果完成则开始执行。

多元依赖:

在这里插入图片描述
依赖多个CompletableFuture 的回调方法包括allOf、anyOf,区别在于allOf 观察者实现类为BiRelay,需要所有被依赖的CF完成后才会执行回调;而anyOf观察者实现类为OrRelay,任意一个被依赖的CF完成后就会触发。二者的实现方式都是将多个被依赖的CF构建成一棵平衡二叉树,执行结果层层通知,直到根节点,触发回调监听。

5、CompletableFuture的方法汇总

在这里插入图片描述

6、使用CompletableFuture的注意点

6.1 代码执行在哪个线程上?

要合理治理线程资源,最基本的前提条件就是要在写代码时,清楚地知道每一行代码 都将执行在哪个线程上。下面我们看一下CompletableFuture的执行线程情况。 CompletableFuture 实现了CompletionStage接口,通过丰富的回调方法,支持各种组合操作,每种组合场景都有同步和异步两种方法。

同步方法(即不带Async后缀的方法)有两种情况。

  • 如果注册时被依赖的操作已经执行完成,则直接由当前线程执行。
  • 如果注册时被依赖的操作还未执行完,则由回调线程执行。

异步方法(即带Async后缀的方法):可以选择是否传递线程池参数Executor运行在指定线程池中;传递时使用指定线程池中的线程,当不传递时,会使用ForkJoinPool中的共用线程池 CommonPool。

例如:

public static void main(String[] args) throws InterruptedException {
    ExecutorService threadPool1 = new ThreadPoolExecutor(10, 10, 0L,
                                                         TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(100));
    CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("supplyAsync 执行线程:" + Thread.currentThread().getName());
        //CommonUtils.sleepSecond(2);
        return "";
    }, threadPool1);
    CommonUtils.sleepSecond(1);
    // 此时,如果future1中的业务操作已经执行完毕并返回,则该thenApply直接由当前main 线程执行;否则,将会由执行以上业务操作的threadPool1中的线程执行。
    future1.thenApply(value -> {
        System.out.println("thenApply 执行线程:" + Thread.currentThread().getName());
        return "thenApply";
    });
    // 使用ForkJoinPool中的共用线程池CommonPool
    future1.thenApplyAsync(value -> {
        System.out.println("thenApplyAsync 执行线程:" + Thread.currentThread().getName());
        return "thenApplyAsync";
    });
    // 使用指定线程池
    future1.thenApplyAsync(value -> {
        System.out.println("threadPool1 执行线程:" + Thread.currentThread().getName());
        return "threadPool1";
    }, threadPool1);
}
supplyAsync 执行线程:pool-1-thread-1
thenApply 执行线程:main
thenApplyAsync 执行线程:ForkJoinPool.commonPool-worker-9
threadPool1 执行线程:pool-1-thread-2
public static void main(String[] args) throws InterruptedException {
    ExecutorService threadPool1 = new ThreadPoolExecutor(10, 10, 0L,
                                                         TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(100));
    CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
        System.out.println("supplyAsync 执行线程:" + Thread.currentThread().getName());
        CommonUtils.sleepSecond(2);
        return "";
    }, threadPool1);
    //CommonUtils.sleepSecond(1);
    // 此时,如果future1中的业务操作已经执行完毕并返回,则该thenApply直接由当前main 线程执行;否则,将会由执行以上业务操作的threadPool1中的线程执行。
    future1.thenApply(value -> {
        System.out.println("thenApply 执行线程:" + Thread.currentThread().getName());
        return "thenApply";
    });
    // 使用ForkJoinPool中的共用线程池CommonPool
    future1.thenApplyAsync(value -> {
        System.out.println("thenApplyAsync 执行线程:" + Thread.currentThread().getName());
        return "thenApplyAsync";
    });
    // 使用指定线程池
    future1.thenApplyAsync(value -> {
        System.out.println("threadPool1 执行线程:" + Thread.currentThread().getName());
        return "threadPool1";
    }, threadPool1);
}
supplyAsync 执行线程:pool-1-thread-1
thenApply 执行线程:pool-1-thread-2
thenApplyAsync 执行线程:ForkJoinPool.commonPool-worker-9
threadPool1 执行线程:pool-1-thread-2

注意:异步回调方法可以选择是否传递线程池参数Executor,建议强制传线程池,且根据实际情况做线程池隔离。 当不传递线程池时,会使用ForkJoinPool中的公共线程池CommonPool,这里所有调用将共用该线程池,核心线程数=处理器数量-1(单核核心线程数为1),所有异步回调都会共用该CommonPool,核心与非核心业务都竞争同一个池中的线程, 很容易成为系统瓶颈。手动传递线程池参数可以更方便的调节参数,并且可以给不同的业务分配不同的线程池,以求资源隔离,减少不同业务之间的相互干扰

6.2 线程池循环引用会导致死锁

public static Object doGet() {
    ExecutorService threadPool1 = new ThreadPoolExecutor(1, 10, 0L,
                                                         TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(100));
    CompletableFuture cf1 = CompletableFuture.supplyAsync(() -> {
        return CompletableFuture.supplyAsync(() -> {
            System.out.println("child");
            return "child-result";
        }, threadPool1).join();// 子任务
    }, threadPool1);
    return cf1.join();
}

如上代码块所示,doGet方法第三行通过supplyAsync向threadPool1请求线程, 并且内部子任务又向threadPool1请求线程。threadPool1核心线程数为1,当有1个请求到达,则threadPool1被打满,子任务请求线程时进入阻塞队列排队, 但是父任务的完成又依赖于子任务,这时由于子任务得不到线程,父任务无法完成。此时就陷入死锁,永远无法恢复。

为了修复该问题,需要将父任务与子任务做线程池隔离,两个任务请求不同的线程池,避免循环依赖导致的阻塞。

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

public static void main(String[] args) {
    CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
        int i = 1 / 0;
        return 0;
    });
    // completableFuture.join();   // 这行代码不加,则不会抛出异常
    // 如果不想获取结果捕获异常,可以在内部使用try...catch...或者使用exceptionally方法
}

6.4 CompletableFuture的get()方法是阻塞的,如果使用它来获取异步调用的返回值,需要添加超时时间,避免无限等待下去

CompletableFuture.get(3, TimeUnit.SECONDS);

6.5 默认线程池的注意点

异步回调方法可以选择是否传递线程池参数Executor,建议传线程池,且根据实际情况做线程池隔离。

当不传递线程池时,会使用ForkJoinPool中的公共线程池CommonPool,这里所有调用将共用该线程池,核心线程数=处理器数量-1(单核核心线程数为1),所有异步回调都会共用该CommonPool,核心与非核心业务都竞争同一个池中的线程, 很容易成为系统瓶颈。手动传递线程池参数可以更方便的调节参数,并且可以给不同的业务分配不同的线程池,以求资源隔离,减少不同业务之间的相互干扰。

7、工具类

待完善
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值