文章目录
参考
【深度解析】万字长文,一文讲透CompletableFuture:Java异步编程的利器!
test_futureTask
public class Test002 {
public static Logger log = LoggerFactory.getLogger(Test002.class);
public static void main(String[] args) {
test_futureTask();
}
private static void test_futureTask() {
// 1. Runnable没有返回值且无声明异常: run():void
// Callable有返回值且有声明异常: call() throws Exception():V
// 2. Runnable能够被线程所执行, Callable不能够被线程所执行
// 3 Future接口定义了与异步任务相关的方法
// cancel(boolean):boolean
// get():V throws InterruptedException, ExecutionException
// 如果任务中发生异常, 此方法将会抛出ExecutionException
// 如果任务正在执行, 尚未执行完, 其它线程调用FutureTask的cancel(true)方法, 此方法将抛出CancelException
// get(long, TimeUnit):V throws InterruptedException, ExecutionException, TimeoutException
// 如果任务中发生异常, 此方法将会抛出ExecutionException
// 如果任务正在执行, 尚未执行完, 其它线程调用FutureTask的cancel(true)方法, 此方法将抛出CancelException
// 如果超时未获取到结果, 此方法将抛出异常
// isCancelled():boolean
// isDone():boolean
// 4. RunnableFuture接口继承了 Runnable接口 和 Future接口, 而 FutureTask实现了 RunnableFuture接口,
// 所以FutureTask还需要传入1个Callable, 才能同时满足 多线程、异步接口、有返回值
Callable<String> callable = () -> {
log.info("come in===>");
return "ok";
};
FutureTask<String> futureTask = new FutureTask<String>(callable);
Thread t1 = new Thread(futureTask, "t1");
t1.start();
}
}
test_futureTaskWithThreadPool
package cn.itcast.redisdemo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.*;
public class Test002 {
public static Logger log = LoggerFactory.getLogger(Test002.class);
public static void main(String[] args) {
test_futureTaskWithThreadPool();
}
private static void test_futureTaskWithThreadPool() {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
log.info("start...");
// FutureTask是Runnable(可以被线程执行)和Future(可以获取异步结果)的组合
// 而传入的Callable的call()方法则是封装了产生结果的逻辑
FutureTask<String> futureTask1 = new FutureTask<String>(()->{
TimeUnit.SECONDS.sleep(1);
return "halo1";
});
FutureTask<String> futureTask2 = new FutureTask<String>(()->{
TimeUnit.SECONDS.sleep(3);
return "halo2";
});
FutureTask<String> futureTask3 = new FutureTask<String>(()->{
TimeUnit.SECONDS.sleep(2);
return "halo3";
});
threadPool.submit(futureTask1);
threadPool.submit(futureTask2);
threadPool.submit(futureTask3);
// 提交1个Callable给线程池, 实际上会把这个Callable传给1个新创建的FutureTask, 并将此FutureTask返回
Callable callable = new Callable() {
@Override
public Object call() throws Exception {
TimeUnit.SECONDS.sleep(2);
return "call";
}
};
Future futureTask4 = threadPool.submit(callable);
// 提交1个Runnable给线程池, 但是通过前面我们知道提交给线程池的会被封装为FutureTask, 而FutureTask需要的是1个Callable,
// 所以这里使用了适配器模式 RunnableAdapter实现了Callable接口, 当执行完run方法后, 直接将此处传入的结果返回
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Future futureTask5 = threadPool.submit(runnable, "run");
try {
futureTask1.get();
futureTask2.get();
futureTask3.get();
futureTask4.get();
futureTask5.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
log.info("end");
}
}
FutureTask的缺点
- 对于简单的业务场景使用Future完全OK
- 回调通知
- 应对Future的完成时间, 完成了可以告诉我, 也就是我们的回调通知
- 通过轮询的方式去判断任务是否完成这样非常占用CPU并且代码不优雅
- 创建异步任务
- Future + 线程池配合
- 多个任务前后依赖可以组合处理
- 想将多个异步任务的计算结果组合起来,后一个异步任务的计算结果需要前一个异步任务的值
- 将两个或多个异步计算合成一个异步计算,这几个异步计算五相独立,同时后面这个又依赖前一个处理的结果
- 对计算速度选最快
- 当Future集合中某个任务最快结束时,返回结果,返回第一名处理结果
FutureTask的缺点: Future对于结果的获取不是很友好, 只能通过阻塞(future.get())或轮询(while(true) + future.isDone())的方式获得任务的结果
- get()方法在Future 计算完成之前会一直处在阻塞状态下
- isDone()方法容易耗费CPU资源
- 对于真正的异步处理我们希望是可以通过传入回调函数,在Future结束时自动调用该回调函数,这样,我们就不用等待结果。
阻塞的方式和异步编程的设计理念相违背,而轮询的方式会耗费无谓的CPU资源。因此,JDK8设计出CompletableFuture。
CompletableFuture提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方
CompletableFuture
CompletableFuture实现了 CompletionStage接口 和 Future接口
CompletionStage接口
代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段,有些类似Linux系统的管道分隔符传参数
- CompletionStage代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段
- 阶段的计算执行可以是一个Function, Consumer或者Runnable,
比如: stage.thenApply(x -> square(x))
.thenAccept(x-> System.out.print(x))
.thenRun(() -> System.out.println()) - 一个阶段的执行可能是被单个阶段的完成触发,也可能是由多个阶段一起触发
CompletableFuture类
- 在Java8中,CompletableFuture提供了非常涯大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合
- CompletableFuture 的方法它可能代表一个明确完成的Future, 也有可能代表一个完成阶段 ( CompletionSstage),它支持在计算完成以后触发一些函数或执行某些动作。
- 它实现了Future和CompletionStage接口
CompletableFuture的4个静态方法
- runAsync无返回值
public static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor) - supplyAsync有返回值
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
executor参数说明
- 没有指定Executor的方法,直接使用默认的ForkJoinPool.commonPool()作为它的线程池执行异步代码。
- 如果指定线程池,则使用我们自定义的或者特别指定的线程池执行异步代码
无返回值示例
ExecutorService threadPool = Executors.newFixedThreadPool(3);
// 一提交, 就开始异步执行了
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
try {
log.info("runAsync无返回值");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, threadPool);
System.out.println(completableFuture.get());
有返回值示例
ExecutorService threadPool = Executors.newFixedThreadPool(3);
// 一提交, 就开始异步执行了
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
try {
log.info("supplyAsync有返回值");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "ok~";
}, threadPool);
// 也可以调用completableFuture.join()来获取结果
System.out.println(completableFuture.get());
Completable减少阻塞和轮询,自动回调
- 从Java8开始引入了CompletableFuture,它是Future的功能增强版,减少阻塞和轮询
- 可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方注
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
log.info("enter...");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (true) throw new RuntimeException("异常1");
return new Random().nextInt(10);
}).whenComplete((result, throwable) -> {
// 1. 不管上一步的任务是否发生异常, 这里都会走
// 2. 这个whenComplete的返回值也是个CompletableFuture,
// 如果上一步任务发生了异常, 那么whenComplete返回的CompletableFuture就是个失败的CompletableFuture, 所以后面如果再接exceptionally就会走exceptionally的逻辑
// 如果上一步任务没有发生异常, 那么whenComplete返回的CompletableFuture就是个成功的CompletableFuture, 所以后面如果再接exceptionally就不会走exceptionally的逻辑
// 总结来说就是这个whenComplete不会影响上个任务的CompletableFuture的状态
// 3. 获取的result是上个任务的结果, throwable是上个任务的异常(如果有的话)
// 4. 这里的whenComplete是去消费结果, 是上一步做完了(正常执行完获得结果而结束 或者是 发生了异常而结束), 再去处理这个做完的结果
log.info("任务完成");
// if (true) throw new RuntimeException("异常2");
if (throwable == null) {
log.info("产生的随机数是: {}", result);
}
}).exceptionally(throwable -> {
// 1. 如果发生了异常, 这里才会走(可以跟js的Promise一起理解, 1个CompletableFuture就是1个Promise, 就看这个CompletableFuture是个什么状态)
// 2. 如果调用此exceptionally方法的CompletableFuture是失败的, 那么会走exceptionally中定义的逻辑,
// 而exceptionally定义的方法返回的是1个新的CompletableFuture, 这个新的CompletableFuture状态与方法的逻辑有关
log.info("任务发生异常:{}, 返回1个默认的值", throwable.getMessage());
// 这里如果抛出异常, 当前的这个exceptionally后面再接exceptionally, 则后面的这个exceptinally收到的异常就是这里抛出来的异常了
// if (true) throw new RuntimeException("异常3");
return 100;
}); // 这里后面还可以继续接whenComplete和exceptionally
log.info("main...");
// 也可以通过CompletableFuture来获取结果
log.info("获取到最终的结果: {}", completableFuture.get());
// 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭
LockSupport.park();
// CompletableFuture的优点
// 1. 异步任务结束时,会自动回调某个对象的方法;
// 2. 主线程设置好回调后,不再关心异步任务的执行,
// 3. 异步任务之间可以顺序执行异步任务出错时,会自动回调某个对象的方法;
CompletableFuture常用方法
1.获得结果和触发计算
@Slf4j
public class Test004 {
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
log.info("enter...");
//暂停几秒钟线程
try {
TimeUnit.SECONDS.sleep(1);
if(true) throw new RuntimeException("异常1");
} catch (Exception e) {
e.printStackTrace();
}
log.info("ending...");
return "abc";
});
// 一直等到有结果为止, 或者任务抛出异常
// System.out.println(completableFuture.get());
// 一直等到有结果为止, 或者任务抛出异常, 或者因超时而抛出超时异常
// System.out.println(completableFuture.get(2L, TimeUnit.SECONDS));
// 一直等到有结果为止, 或者任务抛出异常
// System.out.println(completableFuture.join());
// 立即拿结果不阻塞, 如果没有结果则返回给定值
// System.out.println(completableFuture.getNow("xxx"));
// TimeUnit.MILLISECONDS.sleep(100);
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 尝试使用指定的值作为任务结果来结束任务, 返回值表示当前结束任务操作是否成功了
// (如果任务尚未完成, 任务会继续执行, 但这个任务已经有结果了, 即使后面任务完成也不会修改结果)
boolean triggered = completableFuture.complete("completedVal");
log.info("trigger: {}", triggered);
log.info("结果: {}", completableFuture.get());
LockSupport.park();
}
}
2.对计算结果进行处理
thenApply:计算结果存在依赖关系,使得这两个线程串行化(中间一步有异常时,不走下一步)
handle:计算结果存在依赖关系,使得这两个线程串行化(中间一步有异常时,接着往下处理)
3.对计算结果进行消费
thenAccept:接收上一步任务的处理结果,并消费处理,当前任务无返回结果
注意区别:
- thenApply(需要上一步任务的结果,并且当前任务有返回结果)
- thenRun(不需要上一步任务的结果)
异步线程池:
- 没有传入自定义线程池,都用默认线程池ForkJoinPool:
- 传入了一个自定义线程池的情况
- 如果你执行第一个任务的时候,传入了一个自定义线程池:
- 调用thenRun方法执行第二个任务时,则第二个任务和第一个任务是共用同一个线程池。
- 调用thenRunAsync执行第二个时,则第一个任务使用的是你自己传入的线程池,第二个任务使用的是ForkJoin线程池
- 如果你执行第一个任务的时候,传入了一个自定义线程池:
4.对计算速度进行选用
谁快用谁:applyEither
5.对计算结果进行合并
thenCombine
- 两个CompletionStage任务都完成后,最终能把两个任务的结果一起交给thenCombine来处理
- 先完成的先等着,等待其它分支任务