CompletableFuture常用API整理
一 简介
在读Apache Ratis源码时,发现其中大量使用CompletableFuture以及Functional Inteface,故抽身整理本文,特此注明:本文为个人初学总结,会存在很多问题,非喜勿喷。初始学习视频链接为Youtube
二 Functional Interface
Supplier<T>: T get();
Predicate<T>: boolean test(T);
Function<T, R>: R apply(T);
Consumer<T>: void accept(T)
这四个Functioanl Interface可以表示绝大多数方法,出去Predicate不谈,其余三个分别为
T get() : 没有形参,有返回值
R apply(T): 有形参,有返回值
void accept(T): 有形参,没有返回值
为什么说可以表示绝大多数方法,因为还有一种可能性,就是"没有形参,没有返回值",没错,就是Runnable接口,即void run()方法。
三 CompletableFuture
其中Precdicate接口可以忽略,另外四个(包含run()方法)在CompletableFuture中都能找到对应,分别为
supplyAsync();
runAsync();
thenApply();
thenAccept();
3.1 supplyAsync(Supplier) / runAsync(Runnable)
这两个方法是CompletableFuture的静态方法,用来构造CompletableFutre对象,区别也很明显
- runAsync()方法返回CompletableFuture对象
- supplyAsync()方法返回CompletableFutre对象,其中T为用户指定类型
public class RunAsyncDemo {
public static void main(String[] args) throws InterruptedException {
CompletableFuture<Void> voidFuture = CompletableFuture.runAsync(RunAsyncDemo::doRun);
TimeUnit.MILLISECONDS.sleep(100);
}
private static void doRun(){
System.out.println("no parameter, no return value");
}
}
对应输出为
no parameter, no return value
public class SupplyAsync {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<String> stringFuture = CompletableFuture.supplyAsync(SupplyAsync::doSupply);
System.out.println(stringFuture.get());
}
private static String doSupply(){
return "no parameter, has return value";
}
}
对应输出为
no parameter, has return value
这里需要先解释的是,正如方法名runAsync/supplyAsync,这里是将Runnable/Supplier对象交给CompletableFuture对象,异步执行Runnable/Supplier方法体,所以在构造CompletableFuture之后,想要看到异步执行的效果,要么手动设置休眠时间,要么通过get()方法阻塞等待,避免CompletableFuture还没有执行,main thread执行完毕整体推出。据说CompletableFuture中,有一个Fork-Join线程池,用来执行提交的任务,但是目前还没有深究;另外,在调用CompletableFuture.runAsync()/supplyAsync()方法时,可以传递第二个参数,为用户指定线程池,即CompletableFuture对象不再使用内置的Fork-Join线程池异步执行任务,而是使用用户传递过来的实参线程池。
public class SupplyAsync {
private static ExecutorService es = Executors.newFixedThreadPool(5);
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<String> selfThreadPool = CompletableFuture.supplyAsync(SupplyAsync::doSupply, es);
System.out.println("selfThreadPool's name is: " + selfThreadPool.get());
CompletableFuture<String> defaultThreadPool = CompletableFuture.supplyAsync(SupplyAsync::doSupply);
System.out.println("defaultThreadPool's name is: " + defaultThreadPool.get());
es.shutdown();
}
private static String doSupply(){
return Thread.currentThread().getName();
}
}
对应输出为
selfThreadPool's name is: pool-1-thread-1
defaultThreadPool's name is: ForkJoinPool.commonPool-worker-51
3.2 thenRun() / thenAccept() / thenApply()
首先明确,将CompletableFuture.then…()方法统称为thenMethods()方法,初识CompletableFuture,真的会有点奇怪,CompletableFuture.thenMethods()方法均返回一个CompletableFuture对象,有点像Builder模式,但实际上有着很大的区别,在使用静态内部类Builder时,每一个setter()方法返回的都是同一个Builder对象,即this,但是,每次在调用CompletableFuture.thenMethods()方法后,返回的都是一个新的CompletableFuture对象,这点需要分清,类比我们在构建一个Pipeline,每一个stage都是一个单独的对象。
- thenRun()方法,调用方需为CompletableFuture对象,返回值为CompletableFuture对象
- thenAccept()方法,调用方需为CompletableFuture对象,返回值为CompletableFuture对象
- thenApply()方法,调用方需为CompletableFuture对象,返回值为CompletableFuture对象
public class ThenMethodsDemo {
private static ExecutorService es = Executors.newFixedThreadPool(4);
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> future = create();
CompletableFuture<Void> futureComplete = future.thenApply(data -> data * 3)
.thenAccept(System.out::println)
.thenRun(() -> {
System.out.println("thenRun() method thread name: " + Thread.currentThread().getName());
});
futureComplete.get();
es.shutdown();
System.out.println("main thread exit");
}
private static CompletableFuture<Integer> create(){
return CompletableFuture.supplyAsync(() -> 2, es);
}
}
对应输出为
6
thenRun() method thread name: main
main thread exit
这里还是通过调用futureComplete.get()方法阻塞等待futureComplete执行完毕,避免main thread退出过早。还有一点告诫未来的自己哈,这里的es对象并不是画蛇添足,thenRun()方法体中打印线程名也是点睛之笔。改造一下create()方法
private static CompletableFuture<Integer> create(){
return CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return 2;
}, es);
}
对应输出为
6
thenRun() method thread name: pool-1-thread-1
main thread exit
区别很明显,在create()方法中加上休眠时间,对象线程池名称会成为我们传入的es对象,也就是说执行thenRun()方法的线程并不再是main thread。
这里出现这个问题,其实就是对于thenRun() / thenRunAsync()方法的理解了,thenApply()、thenAccept()、thenRun()方法都有对应的Async的版本,通过度娘很难理清这两组方法的区别,于是查看了API,不幸的是,通过API说明依旧没有分清,如图
按照固有逻辑来讲,thenRun()方法应该是一个阻塞方法,调用方线程会一直等待thenRun()方法执行完成,但是改造完create()方法后,main方法并不是这样执行的,问题在哪里呢,在于thenRun()方法会判断调用者,即CompletableFuture对象是否已经执行完毕。若是,则使用main thread执行thenRun()方法;若否,则将thenRun()方法体丢尽线程池中异步执行,thenRun()方法会立即返回至main thread继续执行。我们可以继续改造ThenMethodDemo类
public class ThenMethodsDemo {
private static ExecutorService es = Executors.newFixedThreadPool(4);
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> future = create();
TimeUnit.MILLISECONDS.sleep(100);
CompletableFuture<Void> futureComplete = future.thenApply(data -> data * 3)
.thenAccept(System.out::println)
.thenRun(() -> {
System.out.println("thenRun() method thread name: " + Thread.currentThread().getName());
});
es.shutdown();
System.out.println("main thread exit");
}
private static CompletableFuture<Integer> create(){
return CompletableFuture.supplyAsync(() -> 2, es);
}
}
这里,我们做了三点改动
- create()方法不再休眠
- 在future构造完毕之后,休眠100ms,确保future能够执行完毕,即futre.get()方法会立即返回结果
- main thread中不再使用futureComplete.get()方法等待futureComplete执行完毕
为什么这么做呢,因为futureComplete在执行thenApply()方法时,会判断futureComplete已经完成,那么就使用main thread阻塞调用future.thenApply()方法,待该stage执行完毕后,调用thenAccept()方法,再次判断调用方CompletableFuture是否完成,实例为完成状态,那么阻塞调用thenAccept()方法,同理,阻塞调用thenRun()方法。故,实际上,整体除去create()方法体是异步执行,其余代码均为一个线程执行,所以在执行打印main thread exit时,所有的pipeline代码均已执行完毕。
综上,thenRun()方法和thenRunAsync()方法的区别如下:
- 当调用者对象还未执行完毕时,即future.get()方法无法立即返回,那么thenRun() / thenRunAysnc()方法执行代码一致,均为异步执行
- 当调用者对象已经执行完毕时,即future.get()方法可以立即返回,那么thenRun()方法会使用调用者线程阻塞执行代码,这也是跟thenRunAsync()方法执行方法体产生分歧的地方。
但是,这里还有一点疑惑,如上代码,为什么在异步执行thenRun()方法时,线程池可以使用到create()方法中传参的es对象,这点还心存疑惑。
3.3 thenRunAsync()/ thenAcceptAsync() / thenApplyAsync()
见3.2,目前还不知道有什么需要补充
3.4 get() / getNow() / Complete() / CompleteOnTimeout()
get()方法在已经贯穿始终,阻塞方法,等待调用者CompletableFuture对象执行完毕,但是对于这种没有timeout机制的方法,向来时不推荐使用的,因为可能要等到花儿都谢了。
由此提供了get((long timeout, TimeUnit unit)方法,可以指定阻塞等待时间,若等待时间结束,还未返回结果,则该方法会抛出TimeoutException;同时提供了getNow(T)方法,如果Completable对象没有执行完成,那么立即返回,返回值为用户传入实参
public class GetDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<String> futureFinish = create()
.thenApplyAsync(data -> data + 2)
.thenApplyAsync(data -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return data + "th day that I have learned the CompletableFuture";
});
String result = null;
try {
result = futureFinish.get(2, TimeUnit.SECONDS);
} catch (TimeoutException e) {
System.out.println("OOPS, timeout");
}finally {
if(result != null){
System.out.println(result);
}
}
}
private static CompletableFuture<Integer> create(){
return CompletableFuture.supplyAsync(() -> 2);
}
}
对应输出结果
OOPS, timeout
将get()方法的2->4,对应输出结果
4th day that I have learned the CompletableFuture
getNow()方法不再演示,没什么好总结的。
接下来时complete(T value)方法,以及completeOnTimeout(T value, long timeout, TimeUnit unit)方法。
按Youtube视频所讲,CompletableFuture有三种状态,Pending、Resolved、Rejected,我理解的为对应为CompletableFuture正在执行中、执行完毕、执行异常,那么complete()方法就是将CompletableFuture从Pending状态手动改为Resolved状态,返回值即实参值
public class CompleteDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> future = new CompletableFuture<>();
CompletableFuture<Void> futureFinish = future.thenApplyAsync(data -> data * 3)
.thenApplyAsync(data -> data + 1)
.thenAccept(System.out::println);
TimeUnit.SECONDS.sleep(3);
System.out.println("main thread exit");
}
}
对应输出结果为
main thread exit
这里,无论等待多长时间,都不会有CompletableFuture的结果打印,原因在于,虽然我们创建了future对象,并构建了pipeline,但是future对象会一直处于Pending状态,并不能转变为Resolved状态,从而触发后续thenMethods()方法的执行,此时可以称此pipeline为inactive状态,这时就需要complete()方法派上用场了,我们可以手动将future的状态改为Resolved,即调用future.complete()方法
public class CompleteDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> future = new CompletableFuture<>();
CompletableFuture<Void> futureFinish = future.thenApplyAsync(data -> data * 3)
.thenApplyAsync(data -> data + 1)
.thenAccept(System.out::println);
//add this line
future.complete(2);
TimeUnit.SECONDS.sleep(3);
System.out.println("main thread exit");
}
}
对应输出为
7
main thread exit
CompleteOnTimeout(T value, long timeout, TimeUnit unit)方法就比较好理解了,等待对应的时间,如果Completable没有完成,那么手动将其状态转换为Resolved,并返回实参value。
3.5 completeExceptionally / exceptionally
刚刚提到过,CompletableFuture有三种状态,对应Pending、Resolved、Rejected,同时CompletableFuture维护了两个Channel,分别为data channel和exception channel,这也是跟Stream不同的地方,众所周知,在Java Stream中,不能很好的处理异常情况,但是CompletableFuture有效避免了此类问题,在thenMethod出错时,CompletableFuture会找pipeline中后续最近的exceptionnally()方法执行,并跳过其中的所有thenMethods()方法,如下图所示,圆括号中一系列thenMethods()方法都不会被执行,待执行完exceptionally()方法后,会跳回至pipeline中data channel中,继续执行后续thenMethod()方法
thenMethod -- thenMethod -- thenMethod (thenMethods) thenMethod -- thenMethod -------- thenMethod
\ / \ /
exceptionally -- exceptionally -- exceptionally -- exceptionally
public class ExceptionallyDemo {
public static void main(String[] args) throws InterruptedException {
CompletableFuture<Void> future = create()
.thenApply(ExceptionallyDemo::plus2)
.thenApply(data -> data + 1)
.thenApply(data -> data + 1)
.exceptionally(ExceptionallyDemo::handleException)
.thenAccept(System.out::println)
.exceptionally(ExceptionallyDemo::handleException2);
TimeUnit.SECONDS.sleep(1);
}
private static int plus2(int a){
int random = new Random().nextInt(10);
if(random % 2 == 0){
return 2 * a;
}else {
throw new RuntimeException("OOPS, something wrong");
}
}
private static int handleException(Throwable throwable){
System.out.println("OOPS, I has caught the exception and resolved it");
return -1;
}
private static Void handleException2(Throwable throwable){
System.out.println("Sry, the exception is beyond my limit");
return null;
}
private static CompletableFuture<Integer> create(){
return CompletableFuture.supplyAsync(() -> 2);
}
}
对应输出结果有两种,分别为
OOPS, I has caught the exception and resolved it
-1
6
到这里,就基本接近尾声了,还剩下一个CompleteExceptionlly()方法,这个方法比较有意思,它并不是将CompletableFuture的状态从Pending改为Resolved,而是从Pending改为Rejected,改造后的代码如下,可以看到completeExceptionally()方法将其状态改为Rejected后,触发了exceptionally()方法
public class ExceptionallyDemo {
public static void main(String[] args) throws InterruptedException {
CompletableFuture<Integer> future = new CompletableFuture<>();
future
// .thenApply(ExceptionallyDemo::plus2)
.thenApply(data -> data + 1)
.thenApply(data -> data + 1)
.exceptionally(ExceptionallyDemo::handleException)
.thenAccept(System.out::println)
.exceptionally(ExceptionallyDemo::handleException2);
future.completeExceptionally(new RuntimeException("OOPS, something wrong by myself"));
TimeUnit.SECONDS.sleep(1);
}
private static int plus2(int a){
int random = new Random().nextInt(10);
if(random % 2 == 0){
return 2 * a;
}else {
throw new RuntimeException("OOPS, something wrong");
}
}
private static int handleException(Throwable throwable){
System.out.println("ERROR: " + throwable);
return -1;
}
private static Void handleException2(Throwable throwable){
System.out.println("Sry, the exception is beyond my limit");
return null;
}
private static CompletableFuture<Integer> create(){
return CompletableFuture.supplyAsync(() -> 2);
}
}
对应输出为
ERROR: java.util.concurrent.CompletionException: java.lang.RuntimeException: OOPS, something wrong by myself
-1