文章目录
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来实现异步回调:
例子:替换文章中的敏感词。
- 读取文章,step1
- 读取敏感词,step2
- 替换敏感词,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 为例,不再枚举全部回调类型。如下图所示:
被观察者:
-
每个CompletableFuture 都可以被看作一个被观察者,其内部有一个 Completion类型的链表成员变量stack,用来存储注册到其中的所有观察者。当被观察者执行完成后会弹栈stack属性,依次通知注册到其中的观察者。上面例子中步骤fn2就是作为观察者被封装在UniApply中。
-
被观察者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、工具类
待完善