玩转CompletableFuture,看这篇就够了
Future
- 在执行比较耗时的任务的时候,我们经常会采取新开线程执行的方式,比如在netty中,如果在io线程中处理耗cpu的计算任务,那么就会造成io线程的堵塞,导致吞吐率的下降(比较好理解,本来io线程可以去处理io的,现在却在等待cpu执行计算任务),这严重影响了io的效率。
- 一般我们采用线程池来执行异步任务,一般情况下不需要获取返回值,但是特殊情况下是需要获取返回值的,也就是需要拿到异步任务的执行结果,举个例子来说:对大量整数进行求和,如果采用多线程来求解,就需要一个汇总线程和多个计算线程,计算线程执行具体的计算任务并且返回求和值,汇总线程进行多个求和值最后的汇总。
- 那么如果我们自己要实现这个异步计算的程序的话可以采用什么方式呢?这实际上是线程之间的通信机制,即我们的汇总线程需要拿到所有计算线程执行完毕的结果,那么我们可以采用共享内存来实现,定义一个全局的map,每个计算线程执行完毕的结果都放到到map中,然后汇总线程从全局map中取出结果进行累加汇总,这样就搞定了,这里面虽然思想很简单,但是还是有一些细节需要考虑的,比如汇总线程怎么判断所有的任务都执行完毕呢?可以通过计算任务的总数和已经完成计算任务的数目进行比较。总之我们肯定可以实现一套这样的异步计算框架。
- 那么进一步抽象,在上面的实现过程中,实际上我们关心的就是每个任务执行的结果,以及任务是否执行完毕,对应到上面提到的计算框架,就是我们关心是否计算完毕和计算完毕后的值,有了这两部分的值,我们的汇总线程就能够很方便的进行计算总的结果了。
- 其实仔细观察,对于几乎所有的异步执行线程,我们都是关心这两部分值的,即任务是否执行完毕,执行完后的结果(如果不需要结果可以返回null),那么这两部分的东西肯定可以抽象出来,避免我们每次编写线程执行的run方法的时候都要自己提交结果和设置完成标志,于是java就是设计了这么一套future机制来帮助开发者。
Future实现机制的具体实现
1、execute方式:
我们知道一个类如果实现了runnable接口,它就能够被线程来执行,因为实现了runnable接口就拥有了run方法,所以能够被执行。所以最简单的异步线程执行方式如下:利用Executors框架来创建一个线程池,然后调用execute方法来提交异步任务,注意这里的execute方法是没有返回的,也就是说我们没法知道提交的任务的执行结果。
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(()->System.out.println("异步执行!"));
2、submit方式:
前面提到的java给我们提供的线程池接口ExecutorService提供了两种提交异步任务的方式,一种就是没有返回值的execute方法(由于ExecutorService接口是extends了Executor接口的,所以拥有了execute方法),还有一种是带有返回值的submit方法。在submit方法中,提供了三个重载方法:
<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
<T> Future<T> submit(Runnable task, T result);
可以看到,submit方法支持实现了callable和runnable的task,不同于runnable只有没有返回值的run方法,callable提供了一个带返回值的call方法,可以有返回值。正是因为runnable没有返回值,所以第二个重载方法返回值为null,第三个重载方法里面可以从外部设置一个返回值,这个返回值将会作为runnable的返回值。具体代码如下
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
public <T> Future<T> submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task, result);
execute(ftask);
return ftask;
}
两个方法都调用newTaskFor方法来创建了一个RunnableFuture的对象,然后调用execute方法来执行这个对象,说明我们线程池真正执行的对象就是这个RunnableFuture对象。
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
由上面代码看出就是创建了一个futureTask对象,这个对象封装了我们提供的runnable和callable对象。futuretask实现了runnablefuture接口,这就是说明futuretask具备了runnable的功能(能被线程执行)和future功能(能够获取自身执行的结果和状态)。能被线程执行功能是我们自己通过实现runnable接口或者callable接口来完成的。future功能前面我们提过是很通用的功能,所以java给我们实现了。下面就进入futuretask查看。
- futuretask对象:futuretask是真正的future功能实现的地方。前面说过这个一个RunnableFuture对象,所以我们看看它的run方法
private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int NORMAL = 2;
private static final int EXCEPTIONAL = 3;
private static final int CANCELLED = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED = 6;
/** 封装的callable对象 */
private Callable<V> callable;
/** task的执行结果 */
private Object outcome;
/** 当前线程池的哪个线程正在执行这个task */
private volatile Thread runner;
/** 等待的线程列表 */
private volatile WaitNode waiters;
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;// 1. 内部包装的一个callable对象
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();// 2. 调用包装的call方法
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);//3. 设置返回值
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
前面提到futuretask是封装了runnable和callable的,可是为什么内部只有一个callable呢,实际上是因为futuretask自己调用适配器转换了一下:代码如下,采用了java的适配器模式。
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}
public static <T> Callable<T> callable(Runnable task, T result) {
if (task == null)
throw new NullPointerException();
return new RunnableAdapter<T>(task, result);
}
static final class RunnableAdapter<T> implements Callable<T> {
final Runnable task;
final T result;
RunnableAdapter(Runnable task, T result) {
this.task = task;
this.result = result;
}
public T call() {
task.run();
return result;
}
} public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}
public static <T> Callable<T> callable(Runnable task, T result) {
if (task == null)
throw new NullPointerException();
return new RunnableAdapter<T>(task, result);
}
static final class RunnableAdapter<T> implements Callable<T> {
final Runnable task;
final T result;
RunnableAdapter(Runnable task, T result) {
this.task = task;
this.result = result;
}
public T call() {
task.run();
return result;
}
}
futuretask的run方法调用了内部封装的callable对象的call方法,获取返回值,并且设置到自己outcome中,state代表执行的状态,这样就通过代理的方式代理了我们的callable的call方法,帮助我们获取执行的结果和状态,所以我们自己编写业务逻辑的时候就不用去管这层通用的逻辑了.
V get() throws InterruptedException, ExecutionException;
boolean cancel(boolean mayInterruptIfRunning);
上面是主要的两个方法,get和cancel,cancel的时候调用runner的interrupt方法即可
if (!(state == NEW &&
UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
return false;
try { // in case call to interrupt throws exception
if (mayInterruptIfRunning) {
try {
Thread t = runner;
if (t != null)
t.interrupt();
} finally { // final state
UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
}
}
} finally {
finishCompletion();
}
return true;
}
get方法的设计是阻塞的,也就是说如果结果没有返回时需要等待的,所以才会有waitnode这个对象的产生,当多个线程都调用futuretask的get方法的时候,如果结果还没产生,就都需要等待,这时候所有等待的线程就会形成一个链表,所以waitnode实际上就是线程的链表。
volatile Thread thread;
volatile WaitNode next;
WaitNode() { thread = Thread.currentThread(); }
}
再看get方法:如果任务没有完成就调用awaitDone进入阻塞,如果完成了直接调用report返回结果
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
throws InterruptedException {
final long deadline = timed ? System.nanoTime() + nanos : 0L;
WaitNode q = null;
boolean queued = false;
for (;;) {
if (Thread.interrupted()) {//1. 如果等待过程中,被中断过了,那么就移除自己
removeWaiter(q);
throw new InterruptedException();
}
int s = state;
if (s > COMPLETING) {
if (q != null)
q.thread = null;
return s;
}
else if (s == COMPLETING) // cannot time out yet
Thread.yield();
else if (q == null)
q = new WaitNode();
else if (!queued)//2. cas更新链表节点
queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
q.next = waiters, q);
else if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
removeWaiter(q);
return state;
}
LockSupport.parkNanos(this, nanos);//3. locksupport原语让线程进入休眠
}
else
LockSupport.park(this);
}
}
还是比较好看懂,其中LockSupport是原语,让线程进行休眠。如果线程在休眠中醒来了,有可能是多种情况,比如get的时间到了,也就是从3中醒来了,这样的话下一次循环就会判断时间到了,从而remove掉节点退出。还有可能等待的线程被interrupt了,这时候就会走到1的逻辑,通过判断中断标记将其remove掉。
既然有了waitnode这个等待链表,那么肯定会有相应的唤醒机制,当执行完毕之后就会将waitnode链表上的线程一次唤醒,如下。
// assert state > COMPLETING;
for (WaitNode q; (q = waiters) != null;) {
if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
for (;;) {
Thread t = q.thread;
if (t != null) {
q.thread = null;
LockSupport.unpark(t);
}
WaitNode next = q.next;
if (next == null)
break;
q.next = null; // unlink to help gc
q = next;
}
break;
}
}
done();
callable = null; // to reduce footprint
}
等待通知机制
所谓等待通知机制,就是某个线程A调用了对象 O 的wait()
方法,另一个线程B调用对象 O 的 notify()
或者 notifyAll()
方法。 线程 A 接收到线程 B 的通知,从wait状态中返回,继续执行后续操作。两个线程通过对象 O 来进行通信。
方法名称 | 描述 |
---|---|
notify() | 随机选择通知一个在对象上等待的的线程,解除其阻塞状态。 |
notfiyAll() | 解除所有那些在该对象上调用wait方法的线程的阻塞状态 |
wait() | 导致线程进入等待状态。 |
wait(long) | 同上,同时设置一个超时时间,线程等待一段时间。 |
wait(long,int) | 同上,且为超时时间设置一个单位。 |
public class waitAndNotify {
private static Object object = new Object();
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread a = new Thread(new waitThread(),"wait");
a.start();
TimeUnit.SECONDS.sleep(5);
Thread b = new Thread(new notifyThread(),"notify");
b.start();
}
static class waitThread implements Runnable{
@Override
public void run() {
synchronized (object){
while (flag){
try {
System.out.println(Thread.currentThread() + "flag is true wait @" +
new SimpleDateFormat("HH:mm:ss").format(new Date()));
object.wait();
}catch (InterruptedException e){
}
}
System.out.println(Thread.currentThread() + "flag is false go on @"+
new SimpleDateFormat("HH:mm:ss").format(new Date()));
}
}
}
static class notifyThread implements Runnable{
@Override
public void run() {
synchronized (object){
System.out.println(Thread.currentThread() + "lock the thread and change flag" +
new SimpleDateFormat("HH:mm:ss").format(new Date()));
object.notify();
flag = false;
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (object){
System.out.println(Thread.currentThread() + "lock the thread again@" +
new SimpleDateFormat("HH:mm:ss").format(new Date()));
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
程序的输出:
Thread[wait,5,main]flag is true wait @22:46:52
Thread[notify,5,main]lock the thread and change flag22:46:57
Thread[notify,5,main]lock the thread again@22:47:02
Thread[wait,5,main]flag is false go on @22:47:07
wait()
和notify()
以及notifyAll()
需要在对象被加锁以后会使用。- 调用
notify()
和notifyAll()
后,对象并不是立即就从wait()
返回。而是需要对象的锁释放以后,等待线程才会从wait()
中返回。
Future 的局限性
- 不能手动完成 当你写了一个函数,用于通过一个远程API获取一个电子商务产品最新价格。因为这个 API 太耗时,你把它允许在一个独立的线程中,并且从你的函数中返回一个 Future。现在假设这个API服务宕机了,这时你想通过该产品的最新缓存价格手工完成这个Future 。你会发现无法这样做。
- Future 的结果在非阻塞的情况下,不能执行更进一步的操作 Future 不会通知你它已经完成了,它提供了一个阻塞的
get()
方法通知你结果。你无法给 Future 植入一个回调函数,当 Future 结果可用的时候,用该回调函数自动的调用 Future 的结果。 - 多个 Future 不能串联在一起组成链式调用 有时候你需要执行一个长时间运行的计算任务,并且当计算任务完成的时候,你需要把它的计算结果发送给另外一个长时间运行的计算任务等等。你会发现你无法使用 Future 创建这样的一个工作流。
- 不能组合多个 Future 的结果 假设你有10个不同的Future,你想并行的运行,然后在它们运行未完成后运行一些函数。你会发现你也无法使用 Future 这样做。
- 没有异常处理 Future API 没有任务的异常处理结构居然有如此多的限制,幸好我们有CompletableFuture,你可以使用 CompletableFuture 达到以上所有目的。
CompletableFuture 实现了 Future
和 CompletionStage
接口,并且提供了许多关于创建,链式调用和组合多个 Future 的便利方法集,而且有广泛的异常处理支持。
CompletableFuture
1、创建CompletableFuture
1、简单的例子
可以使用如下无参构造函数简单的创建 CompletableFuture:
CompletableFuture<String> completableFuture = new CompletableFuture<String>();
这是一个最简单的 CompletableFuture,想获取CompletableFuture 的结果可以使用 CompletableFuture.get()
方法:
completableFuture.complete("Future's Result")
①、使用 runAsync()
运行异步计算
如果你想异步的运行一个后台任务并且不想改任务返回任何东西,这时候可以使用 CompletableFuture.runAsync()
方法,它持有一个Runnable 对象,并返回 CompletableFuture<Void>
。
// Run a task specified by a Runnable Object asynchronously.
CompletableFuture<Void> future = CompletableFuture.runAsync(new Runnable() {
@Override
public void run() {
// Simulate a long-running Job
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
System.out.println("I'll run in a separate thread than the main thread.");
}
});
// Block and wait for the future to complete
future.get()
你也可以以 lambda 表达式的形式传入 Runnable 对象:
// Using Lambda Expression
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
// Simulate a long-running Job
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
System.out.println("I'll run in a separate thread than the main thread.");
});
②、使用 supplyAsync()
运行一个异步任务并且返回结果
当任务不需要返回任何东西的时候,
CompletableFuture.runAsync()
非常有用。但是如果你的后台任务需要返回一些结果应该要怎么样?
CompletableFuture.supplyAsync()
就是你的选择。它持有supplier<T>
并且返回 CompletableFuture<T>
,T
是通过调用 传入的supplier取得的值的类型。
// Run a task specified by a Supplier object asynchronously
CompletableFuture<String> future = CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Result of the asynchronous computation";
}
});
// Block and get the result of the Future
String result = future.get();
System.out.println(result);
Supplier<T>
是一个简单的函数式接口,表示supplier的结果。它有一个get()
方法,该方法可以写入你的后台任务中,并且返回结果。
你可以使用lambda表达式使得上面的示例更加简明:
// Using Lambda Expression
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Result of the asynchronous computation";
});
我们知道runAsync()
和supplyAsync()
方法在单独的线程中执行他们的任务。但是我们不会永远只创建一个线程。 CompletableFuture可以从全局的 ForkJoinPool.commonPool()获得一个线程中执行这些任务。 但是你也可以创建一个线程池并传给runAsync()
和supplyAsync()
方法来让他们从线程池中获取一个线程执行它们的任务。 CompletableFuture API 的所有方法都有两个变体-一个接受Executor
作为参数,另一个不这样:
// Variations of runAsync() and supplyAsync() methods
static CompletableFuture<Void> runAsync(Runnable runnable)
static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
创建一个线程池,并传递给其中一个方法
Executor executor = Executors.newFixedThreadPool(10);
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Result of the asynchronous computation";
}, executor);
2、在 CompletableFuture 转换和运行
CompletableFuture.get()
方法是阻塞的。它会一直等到Future完成并且在完成后返回结果。 但是,这是我们想要的吗?对于构建异步系统,我们应该附上一个回调给CompletableFuture,当Future完成的时候,自动的获取结果。 如果我们不想等待结果返回,我们可以把需要等待Future完成执行的逻辑写入到回调函数中。
可以使用 thenApply()
, thenAccept()
和thenRun()
方法附上一个回调给CompletableFuture。
**①、thenApply() **
可以使用 thenApply()
处理和改变CompletableFuture的结果。持有一个Function<R,T>
作为参数。Function<R,T>
是一个简单的函数式接口,接受一个T类型的参数,产出一个R类型的结果。
// Create a CompletableFuture
CompletableFuture<String> whatsYourNameFuture = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Rajeev";
});
// Attach a callback to the Future using thenApply()
CompletableFuture<String> greetingFuture = whatsYourNameFuture.thenApply(name -> {
return "Hello " + name;
});
// Block and get the result of the future.
System.out.println(greetingFuture.get()); // Hello Rajeev
你也可以通过附加一系列的thenApply()
在回调方法 在CompletableFuture写一个连续的转换。这样的话,结果中的一个 thenApply
方法就会传递给该系列的另外一个 thenApply
方法。
CompletableFuture<String> welcomeText = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Rajeev";
}).thenApply(name -> {
return "Hello " + name;
}).thenApply(greeting -> {
return greeting + ", Welcome to the CalliCoder Blog";
});
System.out.println(welcomeText.get());
// Prints - Hello Rajeev, Welcome to the CalliCoder Blog
**②、thenAccept() 和 thenRun() **
如果你不想从你的回调函数中返回任何东西,仅仅想在Future完成后运行一些代码片段,你可以使用thenAccept()
和 thenRun()
方法,这些方法经常在调用链的最末端的最后一个回调函数中使用。 CompletableFuture.thenAccept()
持有一个Consumer<T>
,返回一个CompletableFuture<Void>
。它可以访问CompletableFuture
的结果:
// thenAccept() example
CompletableFuture.supplyAsync(() -> {
return ProductService.getProductDetail(productId);
}).thenAccept(product -> {
System.out.println("Got product detail from remote service " + product.getName())
});
虽然thenAccept()
可以访问CompletableFuture的结果,但thenRun()
不能访Future的结果,它持有一个Runnable返回CompletableFuture:
// thenRun() example
CompletableFuture.supplyAsync(() -> {
// Run some computation
}).thenRun(() -> {
// Computation Finished.
});
异步回调方法的笔记 CompletableFuture提供的所有回调方法都有两个变体:
// thenApply() variants <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn) <U>
CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn) <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)
这些异步回调变体通过在独立的线程中执行回调任务帮助你进一步执行并行计算。 以下示例:
CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Some Result"
}).thenApply(result -> {
/*
Executed in the same thread where the supplyAsync() task is executed
or in the main thread If the supplyAsync() task completes immediately (Remove sleep() call to verify)
*/
return "Processed Result"
})
在以上示例中,在thenApply()
中的任务和在supplyAsync()
中的任务执行在相同的线程中。任何supplyAsync()
立即执行完成,那就是执行在主线程中(尝试删除sleep测试下)。 为了控制执行回调任务的线程,你可以使用异步回调。如果你使用thenApplyAsync()
回调,将从ForkJoinPool.commonPool()
获取不同的线程执行。
CompletableFuture.supplyAsync(() -> {
return "Some Result"
}).thenApplyAsync(result -> {
// Executed in a different thread from ForkJoinPool.commonPool()
return "Processed Result"
})
此外,如果你传入一个Executor
到thenApplyAsync()
回调中,,任务将从Executor线程池获取一个线程执行。
Executor executor = Executors.newFixedThreadPool(2);
CompletableFuture.supplyAsync(() -> {
return "Some result"
}).thenApplyAsync(result -> {
// Executed in a thread obtained from the executor
return "Processed Result"
}, executor);
3、组合两个CompletableFuture
**①、使用 thenCompose()
组合两个独立的future **
假设你想从一个远程API中获取一个用户的详细信息,一旦用户信息可用,你想从另外一个服务中获取他的贷方。 考虑下以下两个方法getUserDetail()
和getCreditRating()
的实现:
CompletableFuture<User> getUsersDetail(String userId) {
return CompletableFuture.supplyAsync(() -> {
UserService.getUserDetails(userId);
});
}
CompletableFuture<Double> getCreditRating(User user) {
return CompletableFuture.supplyAsync(() -> {
CreditRatingService.getCreditRating(user);
});
}
现在让我们弄明白当使用了thenApply()
后是否会达到我们期望的结果-
CompletableFuture<Double> result = getUserDetail(userId)
.thenCompose(user -> getCreditRating(user));
因此,规则就是-如果你的回调函数返回一个CompletableFuture,但是你想从CompletableFuture链中获取一个直接合并后的结果,这时候你可以使用thenCompose()
。
**②、使用thenCombine()
组合两个独立的 future **
虽然thenCompose()
被用于当一个future依赖另外一个future的时候用来组合两个future。thenCombine()
被用来当两个独立的Future
都完成的时候,用来做一些事情。
System.out.println("Retrieving weight.");
CompletableFuture<Double> weightInKgFuture = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return 65.0;
});
System.out.println("Retrieving height.");
CompletableFuture<Double> heightInCmFuture = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return 177.8;
});
System.out.println("Calculating BMI.");
CompletableFuture<Double> combinedFuture = weightInKgFuture
.thenCombine(heightInCmFuture, (weightInKg, heightInCm) -> {
Double heightInMeter = heightInCm/100;
return weightInKg/(heightInMeter*heightInMeter);
});
System.out.println("Your BMI is - " + combinedFuture.get());
当两个Future都完成的时候,传给``thenCombine()的回调函数将被调用。
4、组合多个CompletableFuture
我们使用thenCompose()
和 thenCombine()
把两个CompletableFuture组合在一起。现在如果你想组合任意数量的CompletableFuture,应该怎么做?我们可以使用以下两个方法组合任意数量的CompletableFuture。
static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)
static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)
①. CompletableFuture.allOf()
CompletableFuture.allOf
的使用场景是当你一个列表的独立future,并且你想在它们都完成后并行的做一些事情。
假设你想下载一个网站的100个不同的页面。你可以串行的做这个操作,但是这非常消耗时间。因此你想写一个函数,传入一个页面链接,返回一个CompletableFuture,异步的下载页面内容。
CompletableFuture<String> downloadWebPage(String pageLink) {
return CompletableFuture.supplyAsync(() -> {
// Code to download and return the web page's content
});
}
现在,当所有的页面已经下载完毕,你想计算包含关键字CompletableFuture
页面的数量。可以使用CompletableFuture.allOf()
达成目的。
List<String> webPageLinks = Arrays.asList(...) // A list of 100 web page links
// Download contents of all the web pages asynchronously
List<CompletableFuture<String>> pageContentFutures = webPageLinks.stream()
.map(webPageLink -> downloadWebPage(webPageLink))
.collect(Collectors.toList());
// Create a combined Future using allOf()
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
pageContentFutures.toArray(new CompletableFuture[pageContentFutures.size()])
);
使用CompletableFuture.allOf()
的问题是它返回CompletableFuture。但是我们可以通过写一些额外的代码来获取所有封装的CompletableFuture结果。
// When all the Futures are completed, call `future.join()` to get their results and collect the results in a list -
CompletableFuture<List<String>> allPageContentsFuture = allFutures.thenApply(v -> {
return pageContentFutures.stream()
.map(pageContentFuture -> pageContentFuture.join())
.collect(Collectors.toList());
});
花一些时间理解下以上代码片段。当所有future完成的时候,我们调用了future.join()
,因此我们不会在任何地方阻塞。
join()
方法和get()
方法非常类似,这唯一不同的地方是如果最顶层的CompletableFuture完成的时候发生了异常,它会抛出一个未经检查的异常。
现在让我们计算包含关键字页面的数量。
// Count the number of web pages having the "CompletableFuture" keyword.
CompletableFuture<Long> countFuture = allPageContentsFuture.thenApply(pageContents -> {
return pageContents.stream()
.filter(pageContent -> pageContent.contains("CompletableFuture"))
.count();
});
System.out.println("Number of Web Pages having CompletableFuture keyword - " +
countFuture.get());
②. CompletableFuture.anyOf()
CompletableFuture.anyOf()
和其名字介绍的一样,当任何一个CompletableFuture完成的时候【相同的结果类型】,返回一个新的CompletableFuture。以下示例:
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Result of Future 1";
});
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Result of Future 2";
});
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "Result of Future 3";
});
CompletableFuture<Object> anyOfFuture = CompletableFuture.anyOf(future1, future2, future3);
System.out.println(anyOfFuture.get()); // Result of Future 2
在以上示例中,当三个中的任何一个CompletableFuture完成, anyOfFuture
就会完成。因为future2
的休眠时间最少,因此她最先完成,最终的结果将是future2
的结果。
CompletableFuture.anyOf()
传入一个Future可变参数,返回CompletableFuture。CompletableFuture.anyOf()
的问题是如果你的CompletableFuture返回的结果是不同类型的,这时候你将会不知道你最终CompletableFuture是什么类型。
5、CompletableFuture 异常处理
我们探寻了怎样创建CompletableFuture,转换它们,并组合多个CompletableFuture。现在让我们弄明白当发生错误的时候我们应该怎么做。
首先让我们明白在一个回调链中错误是怎么传递的。思考下以下回调链:
CompletableFuture.supplyAsync(() -> {
// Code which might throw an exception
return "Some result";
}).thenApply(result -> {
return "processed result";
}).thenApply(result -> {
return "result after further processing";
}).thenAccept(result -> {
// do something with the final result
});
如果在原始的supplyAsync()
任务中发生一个错误,这时候没有任何thenApply
会被调用并且future将以一个异常结束。如果在第一个thenApply
发生错误,这时候第二个和第三个将不会被调用,同样的,future将以异常结束。
①. 使用 exceptionally() 回调处理异常
exceptionally()
回调给你一个从原始Future中生成的错误恢复的机会。你可以在这里记录这个异常并返回一个默认值。
Integer age = -1;
CompletableFuture<String> maturityFuture = CompletableFuture.supplyAsync(() -> {
if(age < 0) {
throw new IllegalArgumentException("Age can not be negative");
}
if(age > 18) {
return "Adult";
} else {
return "Child";
}
}).exceptionally(ex -> {
System.out.println("Oops! We have an exception - " + ex.getMessage());
return "Unknown!";
});
System.out.println("Maturity : " + maturityFuture.get());
②. 使用 handle() 方法处理异常
API提供了一个更通用的方法 - handle()
从异常恢复,无论一个异常是否发生它都会被调用。
Integer age = -1;
CompletableFuture<String> maturityFuture = CompletableFuture.supplyAsync(() -> {
if(age < 0) {
throw new IllegalArgumentException("Age can not be negative");
}
if(age > 18) {
return "Adult";
} else {
return "Child";
}
}).handle((res, ex) -> {
if(ex != null) {
System.out.println("Oops! We have an exception - " + ex.getMessage());
return "Unknown!";
}
return res;
});
System.out.println("Maturity : " + maturityFuture.get());
如果异常发生,res
参数将是 null,否则,ex
将是 null。