CompletableFuture【简介+API】
思维导图
CompletableFuture优缺点
优点
-
异步编程:CompletableFuture提供了一种简单且强大的方式来进行异步编程,可以避免阻塞线程,提高系统的并发性能。
-
链式操作:CompletableFuture支持链式调用,可以通过一系列的操作来组合多个CompletableFuture,实现复杂的异步任务。
-
异常处理:CompletableFuture提供了异常处理的机制,可以通过exceptionally()、handle()等方法来处理异常,保证程序的健壮性。
-
高度可定制性:CompletableFuture提供了丰富的方法来定制异步任务的执行方式,可以设置超时时间、执行顺序、并发级别等。
缺点
5. 学习成本高:CompletableFuture的使用相对复杂,需要掌握一些异步编程的概念和技巧,对于初学者来说学习成本较高。
-
可读性差:由于CompletableFuture支持链式调用,代码可能会变得冗长和难以理解,特别是当有多个异步任务需要组合时。
-
线程资源占用:CompletableFuture在执行异步任务时会占用一定的线程资源,如果不合理地使用CompletableFuture,可能会导致线程资源的浪费。
简介
在Java 8中, 新增加了一个包含50个方法左右的类: CompletableFuture,结合了Future的优点,提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,提供了函数式编程的能力,可以通过回调的方式处理计算结果,并且提供了转换和组合CompletableFuture的方法。 CompletableFuture被设计在Java中进行异步编程。异步编程意味着在主线程之外创建一个独立的线程,与主线程分隔开,并在上面运行一个非阻塞的任务,然后通知主线程进展,成功或者失败。 通过这种方式,你的主线程不用为了任务的完成而阻塞/等待,你可以用主线程去并行执行其他的任务。 使用这种并行方式,极大地提升了程序的表现。
Thread+Runnable(Thread+Callable)
在JDK8之前,我们使用的Java多线程编程,主要是Thread+Runnable来完成,但是这种方式有个弊端就是没有返回值。如果想要返回值怎么办呢,大多数人就会想到Thread+Callable的方式来获取到返回值,例如:
public static void main(String[] args) throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
FutureTask<String> futureTask = new FutureTask<>((Callable<String>) () -> {
TimeUnit.SECONDS.sleep(2);
log.info("thread:{}执行完毕,耗时:{}", Thread.currentThread().getName(), System.currentTimeMillis() - start);
return UUID.randomUUID().toString();
});
new Thread(futureTask).start();
futureTask.isDone();
String s = futureTask.get();
log.info("thread:{}执行完毕,结果:{} 耗时:{}", Thread.currentThread().getName(), s, System.currentTimeMillis() - start);
}
从运行上面代码可以知道当调用代码String s = futureTask.get();
的时候,当前主线程是阻塞状态,另一种方式获取到返回值就是通过轮询futureTask.isDone();
来判断任务是否做完获取返回值。因此JDK8之前提供的异步能力有一定的局限性
•Runnable+Thread虽然提供了多线程的能力但是没有返回值。
•Callable+Thread的方法提供多线程和返回值的能力但是在获取返回值的时候会阻塞主线程。
所以上述的情况只适合不关心返回值,只要提交的Task执行了就可以。另外的就是能够容忍等待。因此我们需要更大的异步能力为了去解决这些痛点问题。比如以下场景:
•两个Task计算合并为一个,这两个异步计算之间相互独立,但是两者之前又有依赖关系。
•对于多个Task,只要一个任务返回了结果就返回结果
CompletableFuture
CompletableFuture是对Future的扩展和增强。CompletableFuture实现了Future接口,并在此基础上进行了丰富的扩展,完美弥补了Future的局限性,同时CompletableFuture实现了对任务编排的能力。借助这项能力,可以轻松地组织不同任务的运行顺序、规则以及方式。从某种程度上说,这项能力是它的核心能力。而在以往,虽然通过CountDownLatch等工具类也可以实现任务的编排,但需要复杂的逻辑处理,不仅耗费精力且难以维护。
CompletableFuture实现了两个接口(如上图所示):Future、CompletionStage。Future表示异步计算的结果,CompletionStage用于表示异步执行过程中的一个步骤(Stage),这个步骤可能是由另外一个CompletionStage触发的,随着当前步骤的完成,也可能会触发其他一系列CompletionStage的执行。从而我们可以根据实际业务对这些步骤进行多样化的编排组合,CompletionStage接口正是定义了这样的能力,我们可以通过其提供的thenAppy、thenCompine等函数式编程方法来组合编排这些步骤。
观察者模式:
CompletableFuture中的每个方法都对应了图中的一个Completion的子类,Completion本身是观察者的基类
cf2 = cf1.thenApply(fn2)
被观察者
1.每个CompletableFuture都可以被看作一个被观察者,其内部有一个Completion类型的链表成员变量stack,用来存储注册到其中的所有观察者。当被观察者执行完成后会弹栈stack属性,依次通知注册到其中的观察者。上面例子中步骤fn2就是作为观察者被封装在UniApply中。
2.被观察者CF中的result属性,用来存储返回结果数据。这里可能是一次RPC调用的返回值,也可能是任意对象
观察者
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
执行过程
q1.在观察者注册之前,如果CF已经执行完成,并且已经发出通知,那么这时观察者由于错过了通知是不是将永远不会被触发
a1: 在注册时检查依赖的CF是否已经完成。如果未完成(即result == null)则将观察者入栈,如果已完成(result != null)则直接触发观察者操作
q2.在”入栈“前会有”result == null“的判断,这两个操作为非原子操作,CompletableFufure的实现也没有对两个操作进行加锁,完成时间在这两个操作之间,观察者仍然得不到通知,是不是仍然无法触发
a2: 入栈之后再次检查CF是否完成,如果完成则触发。
q3:当依赖多个CF时,观察者会被压入所有依赖的CF的栈中,每个CF完成的时候都会进行,那么会不会导致一个操作被多次执行呢
a3: 观察者在执行之前会先通过CAS操作设置一个状态位,将status由0改为1。如果观察者已经执行过了,那么CAS操作将会失败,取消执行。
1 创建异步任务
1.1 supplyAsync
supplyAsync是创建带有返回值的异步任务。它有如下两个方法,一个是使用默认线程池ForkJoinPool.commonPool() 的方法,一个是带有自定义线程池的重载方法
// 带返回值异步请求,默认线程池
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
// 带返回值的异步请求,可以自定义线程池
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
demo:
@Test
public void supplyAsyncTest() throws ExecutionException, InterruptedException {
// forkJoin默认线程池
CompletableFuture<String> forkJoin = CompletableFuture.supplyAsync(() -> "forkJoin");
//等待子任务执行完成
System.out.println("结果->"+ forkJoin.get());
// 自定义线程池
CompletableFuture<String> custom = CompletableFuture.supplyAsync(() -> "custom", Executors.newSingleThreadExecutor());
//等待子任务执行完成
System.out.println("结果->"+ custom.get());
}
1.2 runAsync
runAsync是创建没有返回值的异步任务。它有如下两个方法,一个是使用默认线程池(ForkJoinPool.commonPool())的方法,一个是带有自定义线程池的重载方法
demo
@Test
public void runAsyncTest() throws ExecutionException, InterruptedException {
// forkJoin默认线程池
CompletableFuture<Void> forkJoin = CompletableFuture.runAsync(() -> System.out.println("forkJoin"));
//等待子任务执行完成
System.out.println("结果->"+ forkJoin.get());
// 自定义线程池
CompletableFuture<Void> custom = CompletableFuture.runAsync(() -> System.out.println("custom"), Executors.newSingleThreadExecutor());
//等待子任务执行完成
System.out.println("结果->"+ custom.get());
}
2 异步回调处理
2.1 thenApply和thenApplyAsync
thenApply 表示某个任务执行完成后执行的动作,即回调方法,会将该任务的执行结果即方法返回值作为入参传递到回调方法中,带有返回值。
demo:
@Test
public void thenApplyAsyncTest() throws Exception {
CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
System.out.println((Thread.currentThread() + " cf1 do something...."));
// try {
// TimeUnit.SECONDS.sleep(3);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
return 1;
});
CompletableFuture<Integer> cf2 = cf1.thenApplyAsync((result) -> {
System.out.println((Thread.currentThread() + " cf2 do something...."));
result += 2;
return result;
});
CompletableFuture<Integer> cf3 = cf1.thenApply((result) -> {
System.out.println((Thread.currentThread() + " cf3 do something...."));
result += 2;
return result;
});
System.out.println(cf3.get());
}
thenApplyAsync:执行的线程是从 ForkJoinPool.commonPool() 中获取不同的线程进行执行。
而 thenApply 分两种情况:
如果supplyAsync 执行速度特别快,那么thenApply 就由主线程进行执行。
如果supplyAsync 执行特别慢的话,就由supplyAsync 的线程来执行。
可以用sleep方法来反应supplyAsync 执行速度的快慢。
2.2 thenAccept和thenAcceptAsync
thenAccep表示某个任务执行完成后执行的动作,即回调方法,会将该任务的执行结果即方法返回值作为入参传递到回调方法中,无返回值。线程执行情况同thenApplyAsync
demo
@Test
public void thenAcceptAsyncTest() throws Exception {
StringBuilder stringBuilder = new StringBuilder("我是");
CompletableFuture<Void> future = CompletableFuture.completedFuture("好人").thenAccept((result) -> {
stringBuilder.append(result);
System.out.println(Thread.currentThread().getName());
}).thenAcceptAsync((result) -> {
stringBuilder.append(result);
System.out.println(Thread.currentThread().getName());
});
future.join();
System.out.println(stringBuilder.toString());
}
2.3 thenRun和thenRunAsync
thenRun表示某个任务执行完成后执行的动作,即回调方法,无入参,无返回值。但是不能访问异步线程返回结果,线程执行方案同上
demo
@Test
public void thenRunAsyncTest() throws Exception {
StringBuilder stringBuilder = new StringBuilder("我是");
CompletableFuture<Void> future = CompletableFuture.completedFuture("")
.thenRun(() -> {
stringBuilder.append("好人");
System.out.println(Thread.currentThread().getName());
})
.thenRunAsync(() -> {
stringBuilder.append("好人");
System.out.println(Thread.currentThread().getName());
});
future.join();
System.out.println(stringBuilder.toString());
}
2.4 whenComplete和whenCompleteAsync
whenComplete是当某个任务执行完成后执行的回调方法,会将执行结果或者执行期间抛出的异常传递给回调方法,如果是正常执行则异常为null,回调方法对应的CompletableFuture的result和该任务一致,如果该任务正常执行,则get方法返回执行结果,如果是执行异常,则get方法抛出异常。线程执行情况同上
demo
@Test
public void whenCompleteTest() throws Exception {
CompletableFuture<Integer> cf1 = CompletableFuture
.supplyAsync(() -> 1 / 0)
.whenCompleteAsync((result, e) -> {
System.out.println(Thread.currentThread() + "上个任务结果:" + result);
System.err.println(Thread.currentThread() + "上个任务异常:" + e);
});
System.out.println(cf1.join());
}
2.5 handle和handleAsync
跟whenComplete基本一致,区别在于handle的回调方法有返回值。对于引用传递的处理,通过入参修改可以保证线程安全,直接修改引用则非线程安全
@Test
public void handleAsyncTest() throws Exception {
List<Integer> lists = Lists.newArrayList();
CompletableFuture<List<Integer>> cf = CompletableFuture
.supplyAsync(() -> lists)
.handleAsync((result, e) -> {
for (int i = 0; i < 1000; i++) {
result.add(i);
}
return result;
});
System.out.println(JSON.toJSONString(cf.join().size()));
}
CompletableFuture也提供了catch finally的机制,
CompletableFuture< R > exceptionally(Function<T,R> f):当发生异常的时候,会调用f。
对于whenComplete、handler都有对应的异步调用方法,对应方法为whenCompleteAsync()、handlerAsync()
@Test
public void tryCatchFinallyTest() throws ExecutionException, InterruptedException {
CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> 1 / 0);
f1.exceptionally(throwable -> {
System.out.println(throwable.getMessage());
return 20;
}).whenComplete((r1, throwable) -> System.out.println("complete-----" + r1 + "-----" + throwable));
//返回值与原方法一致
CompletableFuture<Integer> complete = f1.whenComplete((r1, throwable) -> System.out.println("completes-----" + r1 + "-----" + throwable));
CompletableFuture<Integer> handle = f1.handle((r1, throwable) -> {
System.out.println("handle-----" + r1 + "-----" + throwable);
return r1;
});
System.out.println(handle.join());
System.out.println(complete.join());
}
3 多任务# 3.1 thenCombine、thenAcceptBoth 和runAfterBoth
这三个方法都是将两个CompletableFuture组合起来处理,只有两个任务都正常完成时,才进行下阶段任务。
区别:thenCombine会将两个任务的执行结果作为所提供函数的参数,且该方法有返回值;thenAcceptBoth同样将两个任务的执行结果作为方法入参,但是无返回值;runAfterBoth没有入参,也没有返回值。注意两个任务中只要有一个执行异常,则将该异常信息作为指定任务的执行结果
@Test
public void bothTest() throws Exception {
CompletableFuture<Integer> cf1 = CompletableFuture.completedFuture(1);
CompletableFuture<Integer> cf2 = CompletableFuture.completedFuture(2);
CompletableFuture<Integer> cf3 = cf1.thenCombineAsync(cf2, (a, b) -> {
System.out.println(Thread.currentThread() + "我可以拿到a,b的值并组装返回, a=" + a + ",b=" + b);
return a + b;
});
CompletableFuture<Void> cf4 = cf1.thenAcceptBothAsync(cf2, (a, b) -> System.out.println(Thread.currentThread() + "我无返回值,但是可以用StringBuilder等收集"));
CompletableFuture<Void> cf5 = cf1.runAfterBothAsync(cf2, () -> System.out.println(Thread.currentThread() + "我是后置hook函数"));
CompletableFuture.allOf(cf3, cf4, cf5).join();
}
3.2 thenCompose
thenApply和thenCompose都是对一个CompletableFuture返回的结果进行后续操作,返回一个新的CompletableFuture。
<U> CompletionStage<U> thenApply(Function<?super T,?extendsU> fn)
<U> CompletionStage<U> thenCompose(Function<?super T,?extendsCompletionStage<U>> fn)
可以看到,两个方法的返回值都是CompletionStage,不同之处在于它们的传入参数fn.
对于thenApply,fn函数是一个对一个已完成的stage或者说CompletableFuture的的返回值进行计算、操作;
对于thenCompose,fn函数是对另一个CompletableFuture进行计算、操作。
CompletableFuture<String> f1 = CompletableFuture.completedFuture("thenApply")
.thenApply(num -> num + "--done");
CompletableFuture<String> f2 = CompletableFuture.completedFuture("thenCompose")
.thenCompose(s -> CompletableFuture.completedFuture(s + "--done"));
CompletableFuture.allOf(f1, f2).join();
System.out.println(f1.get());
System.out.println(f2.get());
thApply和thenCompose都是将一个CompletableFuture转换为CompletableFuture。不同的是,thenApply中的传入函数的返回值是String,而thenCompose的传入函数的返回值是CompletableFuture。就好像stream中学到的map和flatMap。回想我们做过的二维数组转一维数组,使用stream().flatMap映射时,我们是把流中的每个数据(数组)又展开为了流。
3.3 applyToEither、acceptEither和runAfterEither
这三个方法和上面一样也是将两个CompletableFuture组合起来处理,当有一个任务正常完成时,就会进行下阶段任务。
区别:applyToEither会将已经完成任务的执行结果作为所提供函数的参数,且该方法有返回值;acceptEither同样将已经完成任务的执行结果作为方法入参,但是无返回值;runAfterEither没有入参,也没有返回值。
demo
@Test
public void bothEitherTest() throws Exception {
CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1;
});
CompletableFuture<Integer> cf2 = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 2;
});
cf1.applyToEither(cf2, (result) -> {
System.out.println("接收到" + result);
return result;
});
cf1.acceptEither(cf2, (result) -> {
System.out.println("接收到" + result + "但我无返回值");
});
cf1.runAfterEither(cf2, () -> System.out.println("我是cf1或者cf2的hook函数"));
CompletableFuture.allOf(cf1, cf2).join();
}
3.4 allOf / anyOf
allOf:CompletableFuture是多个任务都执行完成后才会执行,只有有一个任务执行异常,则返回的CompletableFuture执行get方法时会抛出异常,如果都是正常执行,则get返回null。
anyOf :CompletableFuture是多个任务只要有一个任务执行完成,则返回的CompletableFuture执行get方法时会抛出异常,如果都是正常执行,则get返回执行完成任务的结果。
多元依赖:
CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> 1 / 0);
CompletableFuture<Integer> cf2 = CompletableFuture.supplyAsync(() -> 2);
Object anyRes = CompletableFuture.anyOf(cf1, cf2).get();
System.out.println(anyRes);
//返回2
CompletableFuture.allOf(cf1, cf2).get();
// allOf 的get返回null
//多元依赖
CompletableFuture<Void> cf6 = CompletableFuture.allOf(cf3, cf4, cf5);
CompletableFuture<String> result = cf6.thenApply(v -> {
//这里的join并不会阻塞,因为传给thenApply的函数是在CF3、CF4、CF5全部完成时,才会执行 。
result3 = cf3.join();
result4 = cf4.join();
result5 = cf5.join();
//根据result3、result4、result5组装最终result;
return "result";
});
4 总结
•最好传线程池
当不传递线程池时,会使用ForkJoinPool中的公共线程池CommonPool,这里所有调用将共用该线程池,核心线程数=处理器数量-1(单核核心线程数为1),所有异步回调都会共用该CommonPool,核心与非核心业务都竞争同一个池中的线程,很容易成为系统瓶颈。手动传递线程池参数可以更方便的调节参数,并且可以给不同的业务分配不同的线程池,以求资源隔离,减少不同业务之间的相互干扰。
•使用forkjoin是守护线程执行,主线程如果小于子线程执行时间,子线程会直接退出
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(Thread.currentThread() + " 主线程-开始,time->" + formatter.format(new Date()));
CompletableFuture.runAsync(() -> {
System.out.println("子线程-是否为守护线程:" + Thread.currentThread().isDaemon());
System.out.println(Thread.currentThread() + " 子线程-开始,time->" + formatter.format(new Date()));
try {
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + " 子线程-退出,time->" + formatter.format(new Date()));
});
System.out.println(Thread.currentThread() + " 主线程-退出,time->" + formatter.format(new Date()));
用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了
•避免function中循环引用线程池。
public Object doGet() {
ExecutorService threadPool1 = new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(100));
CompletableFuture cf1 = CompletableFuture.supplyAsync(() -> {
//do sth
return CompletableFuture.supplyAsync(() -> {
System.out.println("child");
return "child";
}, threadPool1).join();//子任务
}, threadPool1);
return cf1.join();
}
如上代码块所示,doGet方法第三行通过supplyAsync向threadPool1请求线程,并且内部子任务又向threadPool1请求线程。threadPool1大小为10,当同一时刻有10个请求到达,则threadPool1被打满,子任务请求线程时进入阻塞队列排队,但是父任务的完成又依赖于子任务,这时由于子任务得不到线程,父任务无法完成。主线程执行cf1.join()进入阻塞状态,并且永远无法恢复。
创建类:
•completeFuture 可以用于创建默认返回值
•runAsync 异步执行,无返回值
•supplyAsync 异步执行,有返回值
•anyOf 任意一个执行完成,就可以进行下一步动作
•allOf 全部完成所有任务,才可以进行下一步任务
状态取值类:
•join
合并结果,等待
•get
合并等待结果,可以增加超时时间;
get和join区别,join只会抛出unchecked异常,get会返回具体的异常
•getNow
如果结果计算完成或者异常了,则返回结果或异常;否则,返回valueIfAbsent的值
•isCancelled
判断是否已取消
•isCompletedExceptionally
判断是否未完成get会抛异常
•isDone
判断是否已经完成
控制类:
用于主动控制CompletableFuture的完成行为
•complete
在get的时候如果未完成给一个默认值
•completeExceptionally
设置成功时,未完成状态下返回自定义异常
•cancel
设置后get的时候抛java.util.concurrent.CancellationException,入参无意义
规则总结:
1.以Async结尾的方法,都是异步方法,对应的没有Async则是同步方法,一般都是一个异步方法对应一个同步方法。
2.以Async后缀结尾的方法,都有两个重载的方法,一个是使用内容的forkjoin线程池,一种是使用自定义线程池
3.以run开头的方法,其入口参数一定是无参的,并且没有返回值,类似于执行Runnable方法。
4.以supply开头的方法,入口也是没有参数的,但是有返回值
5.以Accept开头或者结尾的方法,入口参数是有参数,但是没有返回
6.以Apply开头或者结尾的方法,入口有参数,有返回值
7.带有either后缀的方法,表示谁先完成就消费谁