CompletableFuture 用法详解
CompletableFuture 是 Java 8 引入的一个类,位于 java.util.concurrent 包中,用于编写异步代码,提供了一个可编程的、可组合的异步编程框架。以下是 CompletableFuture 的使用环境和具体作用
- 异步编程:比如你有10个任务,每个任务都需要执行挺长时间,开发中通常使用线程池去执行每个任务,
CompletableFuture
支持使用线程池,当然纯粹使用线程池也是可以的 - 非阻塞编程:略
- 流程编程:比如你有10个抽数据的任务,每个任务执行后都需要接着执行记录的任务(如抽取数据的时间,数据量,抽取的数据范围),这就是一个流程(也有人称为回调),利用
CompletableFuture
,在每个任务结束后执行指定的任务
异步编程
先举个例子,熟悉一下使用方式
package com.train;
import java.util.concurrent.Callable;
public class MyThread implements Callable<Integer> {
private Integer count;
private Integer start;
public MyThread(Integer start, Integer count) {
this.start = start;
this.count = count;
}
/**
* 计算从start开始依次相加count次,每次循
* 环后start+1,任务结束后返回最后计算结果
*/
@Override
public Integer call() throws Exception {
int total = 0;
do {
total += start++;
} while ( (--count) > 0 );
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"...over");
return total;
}
}
public class Main {
// 构建一个容纳100个任务的集合
ArrayList<MyThread> threads = new ArrayList<>(){{
for (int i = 0; i < 100; i++)
add(new MyThread(i,i+1));
}};
// 构建一个线程池
ExecutorService executor = Executors.newFixedThreadPool(threads.size());
/**
* 遍历集合,利用CompletableFuture把每个任务提交给
* 线程池执行,并控制在每个任务结束后打印任务执行的结果
*/
threads.forEach(thread->{
CompletableFuture.supplyAsync(() -> {
try {
return thread.call();// 执行任务
} catch (Exception e) {
throw new RuntimeException(e);
}
}, executor).thenAccept( i -> {
System.out.println("计算结果:"+i);
});
});
}
执行
从上面的代码示例,我使用了.supplyAsync
方法,CompletableFuture
还提供了.runAsync
方法,这两种方法的区别如下
- supplyAsync 与 runAsync 相同点
- Runnable中的代码抛出异常,这个异常会被封装到 CompletableFuture 中,可以通过 exceptionally 方法来处理
- 不指定线程池,则默认使用 ForkJoinPool.commonPool() 执行任务
- supplyAsync 特点
- 执行需要返回结果的异步任务。它接受 Supplier<U> 任务为参数。【注:Supplier<U> 可返回值】
上述示例的这一块代码,其实就是一个Supplier<U>任务,只不过用了lambda表达式而已 () -> { try { return thread.call();// 执行任务 } catch (Exception e) { throw new RuntimeException(e); } }
- 执行需要返回结果的异步任务。它接受 Supplier<U> 任务为参数。【注:Supplier<U> 可返回值】
- runAsync 特点
- 用于执行一个不需要返回结果的异步任务。它接受Runnable的任务为参数【注:Runnable 没有返回值】
总结:归纳起来说,两者的不同就是执行是否有返回值的任务
用runAsync改写示例代码
threads.forEach(thread->{
CompletableFuture.runAsync(() -> {
try {
thread.call();//这里不许用return
} catch (Exception e) {
throw new RuntimeException(e);
}
}, executor).thenRun(()->{
System.out.println("计算结束");
});
});
回调
看改写后的代码可知,因为runAsync
并不返回值,所以我用了thenRun
执行之后的回调逻辑,那么thenRun
和thenAccept
的区别是啥呢?
- thenRun 和 thenAccept 相同点
- 用于处理异步操作的结果
- 使用lambda表达式替代参数时,lambda的实现不用返回
- thenRun 特点
- 接受一个 Runnable 对象为参数
- 在异步操作完成时执行,不接受任何参数
- 方法不返回任何值
- thenAccept 特点
- 接受一个 Consumer
对象作为参数 - 在异步操作完成时执行,并接受一个参数,即异步操作的结果
- thenAccept 方法返回一个新的 CompletableFuture
,表示异步操作的结果已经被消费,但是呢就我个人使用而言:这个返回和没返回一个样,为啥这么说呢,首先我们看看它的源码,如下
由此可知,返回一个包装public CompletableFuture<Void> thenAccept(Consumer<? super T> action) { return uniAcceptStage(null, action); }
Void
型数据的CompletableFuture
,这就是说返回的数据类型是固定的,返回跟没返回一个样~
- 接受一个 Consumer
上述的 thenRun 和 thenAccept 方法都是不返回业务数据的(即使thenAccept返回一个固定的数据类型,我们就当做是不返回吧)
既然thenRun
和thenAccept
都返回不了业务数据,CompletableFuture当然也支持返回自定义业务数据的回调方法——thenApplyAsync
和thenApply
改写示例
public static void main(String[] args) throws ExecutionException, InterruptedException {
ArrayList<MyThread> threads = new ArrayList<>(){{
for (int i = 0; i < 10; i++) {
add(new MyThread(i,i+1));
}
}};
ArrayList<CompletableFuture<Integer>> arr = new ArrayList<>();
ExecutorService executor = Executors.newFixedThreadPool(threads.size());
threads.forEach(thread->{
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
try {
return thread.call();
} catch (Exception e) {
throw new RuntimeException(e);
}
}, executor)
.thenApplyAsync(result -> {
// 回调执行逻辑
System.out.println("计算结束:" + result);
return result;
});
arr.add(future);
});
// 等待所有的任务都执行完毕
arr.forEach(CompletableFuture::join);
// 遍历结果
arr.forEach(item -> {
try {
System.out.println(item.get());
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
});
}
thenApplyAsync
和thenApply
区别
- thenApplyAsync 和 thenApply 相同点
- 返回一个新的 CompletableFuture<U>,其中包含函数的执行结果
- thenApplyAsync 特点
- 异步执行,除非开发者提供了一个自定义线程池,否则它使用默认的 ForkJoinPool 线程池来执行提供的函数 fn
- thenApply 特点
- 同步执行,它在当前线程(即调用 thenApply 的线程)上执行提供的函数 fn
总结:thenApply 和 thenApplyAsync 的区别就是其用什么线程执行提供的函数fn,当然还有
thenAcceptAsync
和thenRunAsync
,这俩的差别和特点就请读者参考前面的解释举一反三了。
allOf 方法(用于做总收尾的回调)
它用于等待多个 CompletableFuture 实例中的所有异步操作完成。当你有多个并行的异步任务时,这个方法非常有用,因为它允许你等待所有任务完成,然后再继续执行后续的代码,接示例如下:
CompletableFuture.allOf(arr.toArray(new CompletableFuture[arr.size()])).thenRun(()->{
System.out.println("执行完毕");
});
anyOf 方法(众任务中一个任务完成即回调)
它用于等待多个 CompletableFuture 实例中的某一个异步操作完成。当你有多个并行的异步任务时,只要有一个成功完成,即调用回调,接示例如下:
CompletableFuture.allOf(arr.toArray(new CompletableFuture[arr.size()])).thenRun(()->{
System.out.println("某一个执行完毕");
});
join 阻塞
它用于等待异步操作完成,并返回操作的结果。这个方法是 CompletableFuture 的阻塞获取操作,它会暂停当前线程直到 CompletableFuture 完成才执行之后的代码
总结
执行异步任务
supplyAsync
runAsync
异步回调
thenApply
和thenApplyAsync
thenAccept
和thenAcceptAsync
thenRun
和thenRunAsync
多CompletableFuture联合处理
allOf
anyOf
阻塞操作
join
CompletableFuture 还有许多方法,后面有时间总结补上,先就这样,普通开发足够了
流程编程
举个例子,如果有3个任务:jobA,jobB,jobC,这3个任务必须按顺序执行,用CompletableFuture
链式调用
在 CompletableFuture 的链式调用中,每个 thenAccept 方法都是在前一个 CompletableFuture 完成之后执行的。这意味着,
对于同一个 CompletableFuture 实例,注册的回调(thenAccept、thenApply 等)会按照它们被添加到链中的顺序依次执行。
如果你使用了 thenAccept,那么所有的回调参数均是一样的,如果在 thenAccept 方法中修改了结果,此修改对于后续的 thenAccept 方法是有影响的
如果你使用了 thenApply,那么你需要在回调方法中返回一个自定义的结果,这个结果将作为下一个回调方法的参数被传入。
使用thenAccept
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> jobA(), executor);
future.thenAccept(data -> jobB(data));
future.thenAccept(data -> jobC(data));
使用thenApply
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> jobA(), executor);
future.thenApply(data -> jobB(data));
// 此回调中的参数就是jobB执行后返回的结果
future.thenApply(data -> jobC(data));
或者写为
CompletableFuture<String> future = CompletableFuture.supplyAsync(() ->jobA(), executor)
.thenAccept(data -> jobB(data))
.thenAccept(data -> jobC(data));
本文来自博客园,作者:勤匠,转载请注明原文链接:https://www.cnblogs.com/JarryShu/articles/18520302
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现