1:背景
我们把Runnable理解为最基本的线程任务,只具备在线程下执行一段逻辑的能力。为了获取执行的返回值,创造了Callable和与其配合使用的Future。future 通过提交一个 callable 任务给线程池,线程池后台启动其他线程去执行,然后再调用 get() 方法获取结果
private void test() {
ExecutorService executor = Executors.newCachedThreadPool();
Future<Integer> future = executor.submit(() -> sleep(1));
try {
Integer integer = future.get(3, TimeUnit.SECONDS);
System.out.println(integer);
} catch (InterruptedException e) {
// 当前线在等待中被中断
e.printStackTrace();
} catch (ExecutionException e) {
// 任务执行中的异常
e.printStackTrace();
} catch (TimeoutException e) {
// 超时
e.printStackTrace();
}
}
private int sleep(int timeout) {
try {
TimeUnit.SECONDS.sleep(timeout);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1;
}
为了将任务之间进行逻辑编排,就诞生了CompletableFuture。关于如何理解任务的逻辑编排,举一个简单的例子:
打开电脑-更新系统这两个操作是有先后顺序的,但是泡茶和这两个操作没有先后顺序,是可以并行的,而开始办公必须要等待其他操作结束之后才能进行,这就形成了任务编排的执行链。
在IO密集型系统中,类似的场景有很多。因为不同数据集的查询依赖主键不同,A数据集的查询主键是B数据集的一个字段这种情况很常见,通常还需要并发查询多个数据集的数据,所以对于多线程的执行编排是有需求的。
一种解决办法是CountDownLatch,让线程执行到某个地方后进行等待,直到依赖的任务执行结束。对于一些简单的执行链是可以满足的,但是当编排逻辑复杂起来,CountDownLatch会导致代码难以维护和调试。所以诞生了CompletableFuture用来描述和维护任务之间的依赖关系以进行任务编排。
2:初步了解使用方式
2.1:创建与执行
同步方法:
和FutureTask类似,CompletableFuture也通过get()方法获取执行结果。但是不同的是,CompletableFuture本身可以不承载可执行的任务(相比FutureTask则必须承载一个可执行的任务Callable),通过一个用于标记执行成功并设置返回值的函数,在使用上也更为灵活,如下:
CompletableFuture<String> demo = new CompletableFuture<>();
demo.complete("success");
1System.out.println(demo.get());
执行结果:success
和Future类似,get()函数也是同步阻塞的,调用get函数后线程会阻塞直到调用complete方法标记任务已经执行成功。
除了手动触发任务的完成,也可以让创建对象的同时就标记任务完成:
CompletableFuture<String> demo = CompletableFuture.completedFuture("success");
System.out.println(demo.get());
执行结果:success
异步方法:
相比于同步方法,异步执行更为常见。比如下面这个例子:
CompletableFuture<String> demo = CompletableFuture.supplyAsync(() -> {
System.out.println("do something by thread" + Thread.currentThread().getName());
return "success";
});
System.out.println(demo.get());
执行结果:
do something by threadForkJoinPool.commonPool-worker-9
success
supplyAsync方法接收一个Supplier对象,逻辑函数交给线程池中的线程异步执行
默认会使用ForkJoinPool的公共线程池来执行代码(不推荐),当然也可以指定线程池,如下:
ExecutorService executor = Executors.newFixedThreadPool(4);
CompletableFuture<String> demo = CompletableFuture.supplyAsync(() -> {
System.out.println("do something by thread" + Thread.currentThread().getName());
return "success";
}, executor);
System.out.println(demo.get());
执行结果:
do something by threadpool-1-thread-1
success
如果不需要执行结果,也可以用runAsync方法:
CompletableFuture.runAsync(() -> {
System.out.println("do something by thread" + Thread.currentThread().getName());
});
执行结果:
do something by threadForkJoinPool.commonPool-worker-9
2.2:多任务编排
多任务编排是CompletableFuture的核心,这里列举不同的场景来进行说明
一元依赖
步骤2需要依赖步骤1执行完毕才能执行,类似主线程的顺序执行,可以通过以下方式实现:
ExecutorService executor = Executors.newFixedThreadPool(4);
CompletableFuture<String> step1 = CompletableFuture.supplyAsync(() -> {
System.out.println("执行【步骤1】");
return "【步骤1的执行结果】";
}, executor);
CompletableFuture<String> step2 = step1.thenApply(result -> {
System.out.println("上一步操作结果为:" + result);
return "【步骤2的执行结果】";
});
System.out.println("步骤2的执行结果:" + step2.get());
执行结果:
执行【步骤1】
上一步操作结果为:【步骤1的执行结果】
步骤2的执行结果:【步骤2的执行结果】
通过thenApply方法接收上一个CompletableFuture对象的返回值,其中隐含的逻辑是,该处逻辑只有等上一个CompletableFuture对象执行完后才会执行
二元依赖
相比于一元依赖的顺序执行链,二元依赖更为常见,比如下面这个场景:
步骤1和2是并行的,而步骤3需要等步骤1和2执行完之后才能执行,通过CompletableFuture是这么实现的:
ExecutorService executor = Executors.newFixedThreadPool(4);
CompletableFuture<String> step1 = CompletableFuture.supplyAsync(() -> {
System.out.println("执行【步骤1】");
return "【步骤1的执行结果】";
}, executor);
CompletableFuture<String> step2 = CompletableFuture.supplyAsync(() -> {
System.out.println("执行【步骤2】");
return "【步骤2的执行结果】";
}, executor);
CompletableFuture<String> step3 = step1.thenCombine(step2, (result1, result2) -> {
System.out.println("前两步操作结果分别为:" + result1 + result2);
return "【步骤3的执行结果】";
});
System.out.println("步骤3的执行结果:" + step3.get());
执行结果:
执行【步骤1】
执行【步骤2】
前两步操作结果分别为:【步骤1的执行结果】【步骤2的执行结果】
步骤3的执行结果:【步骤3的执行结果】
通过thenCombine方法等待step1和step2都执行完毕后,获取其返回结果并执行一段新的逻辑
多元依赖
当然还可能有下面这种场景,步骤M需要依赖1-N的N个前置节点:
这种情况会更为复杂,实现方式如下:
ExecutorService executor = Executors.newFixedThreadPool(4);
CompletableFuture<String> step1 = CompletableFuture.supplyAsync(() -> {
System.out.println("执行【步骤1】");
return "【步骤1的执行结果】";
}, executor);
CompletableFuture<String> step2 = CompletableFuture.supplyAsync(() -> {
System.out.println("执行【步骤2】");
return "【步骤2的执行结果】";
}, executor);
CompletableFuture<String> step3 = CompletableFuture.supplyAsync(() -> {
System.out.println("执行【步骤3】");
return "【步骤3的执行结果】";
}, executor);
CompletableFuture<Void> stepM = CompletableFuture.allOf(step1, step2, step3);
CompletableFuture<String> stepMResult = stepM.thenApply(res -> {
// 通过join函数获取返回值
String result1 = step1.join();
String result2 = step2.join();
String result3 = step3.join();
return result1 + result2 + result3;
});
System.out.println("步骤M的结果:" + stepMResult.get());
执行结果:
执行【步骤1】
执行【步骤2】
执行【步骤3】
步骤M的结果:【步骤1的执行结果】【步骤2的执行结果】【步骤3的执行结果】
通过allOf函数声明当参数中的所有任务执行完毕后,才会执行下一步操作,但是要注意,allOf本身只是定义节点,真正阻塞的位置是thenApply函数。
和之前的方式不同,由于采用了不定变量,所以要通过CompletableFuture#join来获取每个任务的返回值。
除了allOf之外,如果我们需要任意一个任务完成后就执行下一步操作,可以使用anyOf方法,如下:
// step1/2/3的定义相同
// ...
CompletableFuture<Object> stepM = CompletableFuture.anyOf(step1, step2, step3);
System.out.println("步骤M的结果:" + stepM.get());
执行结果:
步骤M的结果:【步骤1的执行结果】
与allOf不同,anyOf的返回值即为第一个执行完毕的任务
3:工作原理
3.1:概念
在讲原理之前,先来了解一下CompletableFuture的定义。
CompletableFuture是Java 8 中新增的一个类,它是对Future接口的扩展。从下方的类继承关系图中我们看到其不仅实现了Future接口,还有CompletionStage接口,当Future需要显示地完成时,可以使用CompletionStage接口去支持完成时触发的函数和操作,当2个以上线程同时尝试完成、异常完成、取消一个CompletableFuture时,只有一个能成功。
CompletableFuture主要作用就是简化我们异步编程的复杂性,支持函数式编程,可以通过回调的方式处理计算结果。
3.2:为什么会有CompletableFuture
在java5中,JDK为我们提供了Callable和Future,使我们可以很容易的完成异步任务结果的获取,但是通过Future的get获取异步任务结果会导致主线程的阻塞,这样在某些场景下是非常消耗CPU资源的,进而Java8为我们提供了CompletableFuture,使我们无需阻塞等待,而是通过回调的方式去处理结果,并且还支持流式处理、组合异步任务等操作。
3.3:下面我们就CompletableFuture 的使用进行简单分类:
- 创建任务
- supplyAsync/runAsync
- 异步回调
- thenApply/thenAccept/thenRun
- thenApplyAsync/thenAcceptAsync/thenRunAsync
- exceptionally
- handle/whenComplete
- 组合处理
- thenCombine / thenAcceptBoth / runAfterBoth
- applyToEither / acceptEither / runAfterEither
- thenCompose
- allOf / anyOf
具体内容请参照以下案例:
public static void main(String[] args) throws Exception {
// 1.带返回值的异步任务(不指定线程池,默认ForkJoinPool.commonPool(),单核ThreadPerTaskExecutor)
CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
return 1 + 1;
});
System.out.println("cf1 result: " + cf1.get());
// 2.无返回值的异步任务(不指定线程池,默认ForkJoinPool.commonPool(),单核ThreadPerTaskExecutor)
CompletableFuture cf2 = CompletableFuture.runAsync(() -> {
int a = 1 + 1;
});
System.out.println("cf2 result: " + cf2.get());
// 3.指定线程池的带返回值的异步任务,runAsync同理
CompletableFuture<Integer> cf3 = CompletableFuture.supplyAsync(() -> {
return 1 + 1;
}, Executors.newCachedThreadPool());
System.out.println("cf3 result: " + cf3.get());
// 4.回调,任务执行完成后执行的动作
CompletableFuture<Integer> cf4 = cf1.thenApply((result) -> {
System.out.println("cf4回调拿到cf1的结果 result : " + result);
return result + 1;
});
System.out.println("cf4 result: " + cf4.get());
// 5.异步回调(将回调任务提交到线程池),任务执行完成后执行的动作后异步执行
CompletableFuture<Integer> cf5 = cf1.thenApplyAsync((result) -> {
System.out.println("cf5回调拿到cf1的结果 result : " + result);
return result + 1;
});
System.out.println("cf5 result: " + cf5.get());
// 6.回调(同thenApply但无返回结果),任务执行完成后执行的动作
CompletableFuture cf6 = cf1.thenAccept((result) -> {
System.out.println("cf6回调拿到cf1的结果 result : " + result);
});
System.out.println("cf6 result: " + cf6.get());
// 7.回调(同thenAccept但无入参),任务执行完成后执行的动作
CompletableFuture cf7 = cf1.thenRun(() -> {
});
System.out.println("cf7 result: " + cf7.get());
// 8.异常回调,任务执行出现异常后执行的动作
CompletableFuture<Integer> cf = CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("出现异常");
});
CompletableFuture<Integer> cf8 = cf.exceptionally((result) -> {
return -1;
});
System.out.println("cf8 result: " + cf8.get());
// 9.当某个任务执行完成后执行的回调方法,会将执行结果或者执行期间抛出的异常传递给回调方法
// 如果是正常执行则异常为null,回调方法对应的CompletableFuture的result和该任务一致;
// 如果该任务正常执行,则get方法返回执行结果,如果是执行异常,则get方法抛出异常。
CompletableFuture<Integer> cf9 = cf1.handle((a, b) -> {
if (b != null) {
b.printStackTrace();
}
return a;
});
System.out.println("cf9 result: " + cf9.get());
// 10 与handle类似,无返回值
try {
CompletableFuture<Integer> cf10 = cf.whenComplete((a, b) -> {
if (b != null) {
b.printStackTrace();
}
});
System.out.println("cf10 result: " + cf10.get());
} catch (Exception e) {
System.out.println("cf10 出现异常!!!");
}
// 11 组合处理(两个都完成,然后执行)有入参,有返回值
CompletableFuture<Integer> cf11 = cf1.thenCombine(cf3, (r1, r2) -> {
return r1 + r2;
});
System.out.println("cf11 result: " + cf11.get());
// 12 组合处理(两个都完成,然后执行)有入参,无返回值
CompletableFuture cf12 = cf1.thenAcceptBoth(cf3, (r1, r2) -> {
});
System.out.println("cf12 result: " + cf12.get());
// 13 组合处理(两个都完成,然后执行)无入参,无返回值
CompletableFuture cf13 = cf1.runAfterBoth(cf3, () -> {
});
System.out.println("cf13 result: " + cf13.get());
// 14 组合处理(有一个完成,然后执行)有入参,有返回值
CompletableFuture<Integer> cf14 = cf1.applyToEither(cf3, (r) -> {
return r;
});
System.out.println("cf14 result: " + cf14.get());
// 15 组合处理(有一个完成,然后执行)有入参,无返回值
CompletableFuture cf15 = cf1.acceptEither(cf3, (r) -> {
});
System.out.println("cf15 result: " + cf15.get());
// 16 组合处理(有一个完成,然后执行)无入参,无返回值
CompletableFuture cf16 = cf1.runAfterEither(cf3, () -> {
});
System.out.println("cf16 result: " + cf16.get());
// 17 方法执行后返回一个新的CompletableFuture
CompletableFuture<Integer> cf17 = cf1.thenCompose((r) -> {
return CompletableFuture.supplyAsync(() -> {
return 1 + 1;
});
});
System.out.println("cf17 result: " + cf17.get());
// 18 多个任务都执行成功才会继续执行
CompletableFuture.allOf(cf1,cf2,cf3).whenComplete((r, t) -> {
System.out.println(r);
});
// 18 多个任务任意一个执行成功就会继续执行
CompletableFuture.anyOf(cf1,cf2,cf3).whenComplete((r, t) -> {
System.out.println(r);
});
}