CompletableFuture从入门到精通?算了,入个门就行了
Future vs CompletableFuture#
准备工作#
为了便于后续更好地调试和学习,我们需要定义一个工具类CommonUtils
辅助我们对知识的理解。这个工具类总共四个方法
readFile
:读取指定路径的文件内容sleepMillis
:休眠指定的毫秒数sleepSecond
:休眠指定的秒数printThreadLog
:打印携带线程信息的日志信息
object CommonUtils {
fun readFile(pathToFile: String): String {
return try {
Files.readString(pathToFile.let { Paths.get(it) })
} catch (e: Exception) {
e.printStackTrace()
""
}
}
fun sleepMillis(millis: Long) {
try {
TimeUnit.MILLISECONDS.sleep(millis)
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
fun sleepSecond(seconds: Int) {
try {
TimeUnit.SECONDS.sleep(seconds.toLong())
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
fun printThreadLog(message: String?) {
val result = StringJoiner(" | ")
.add(System.currentTimeMillis().toString())
.add(String.format("%2d", Thread.currentThread().id))
.add(Thread.currentThread().name.toString())
.add(message)
.toString()
println(result)
}
}
Future 的局限性#
需求:替换新闻稿 ( news.txt ) 中敏感词汇 ,把敏感词汇替换成*,敏感词存储在 filter_words.txt 中
news.txt
oh my god!completablefuture真tmd好用
filter_words.txt
尼玛,SB,tmd
fun main(args: Array<String>) {
val executor = Executors.newFixedThreadPool(5)
// step1: 读取敏感词汇 thread1
val filterWordFuture = executor.submit<Array<String>> {
val str: String = CommonUtils.readFile("filter_words.txt")
str.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
}
// step2: 读取新闻稿 thread2
val newsFuture: Future<String> = executor.submit<String> { CommonUtils.readFile("news.txt") }
// step3 : 替换操作 thread3
val replaceFuture = executor.submit<String> {
val words = filterWordFuture.get()
var news = newsFuture.get()
for (word in words) {
if (news.indexOf(word) > 0) {
news = news.replace(word, "**")
}
}
news
}
// step 4: 打印输出替换后的新闻稿 main
val filteredNews = replaceFuture.get()
println("filteredNews=$filteredNews")
executor.shutdown()
}
通过上面的代码,我们会发现,Future相比于所有任务都直接在主线程处理,有很多优势,但同时也存在不足,至少表现如下:
- 在没有阻塞的情况下,无法对Future的结果执行进一步的操作。Future不会告知你它什么时候完成,你如果想要得到结果,必须通过一个get()方法,该方法会阻塞直到结果可用为止。 它不具备将回调函数附加到Future后并在Future的结果可用时自动调用回调的能力。
- 无法解决任务相互依赖的问题。filterWordFuture和newsFuture的结果不能自动发送给replaceFuture,需要在replaceFuture中手动获取,所以使用Future不能轻而易举地创建异步工作流。
- 不能将多个Future合并在一起。假设你有多种不同的Future,你想在它们全部并行完成后然后再运行某个函数,Future很难独立完成这一需要。
- 没有异常处理。Future提供的方法中没有专门的API应对异常处理,还是需要开发者自己手动异常处理。
CompletableFuture 的优势#
CompletableFuture 实现了Future和CompletionStage接口
CompletableFuture 相对于 Future 具有以下优势:
- 为快速创建、链接依赖和组合多个Future提供了大量的便利方法。
- 提供了适用于各种开发场景的回调函数,它还提供了非常全面的异常处理支持。
- 无缝衔接和亲和 lambda 表达式 和 Stream - API 。
- 我见过的真正意义上的异步编程,把异步编程和函数式编程、响应式编程多种高阶编程思维集于一身,设计上更优雅。
创建异步任务#
runAsync#
如果你要异步运行某些耗时的后台任务,并且不想从任务中返回任何内容,则可以使用CompletableFuture.runAsync()
方法。它接受一个Runnable接口的实现类对象,方法返回CompletableFuture<Void>
对象
static CompletableFuture<Void> runAsync(Runnable runnable);
演示案例:开启一个不从任务中返回任何内容的CompletableFuture异步任务
fun main() {
CommonUtils.printThreadLog("main start")
// 使用Lambda表达式
CompletableFuture.runAsync {
CommonUtils.printThreadLog("读取文件开始");
// 使用睡眠来模拟一个长时间的工作任务(例如读取文件,网络请求等)
CommonUtils.sleepSecond(3);
CommonUtils.printThreadLog("读取文件结束");
}
CommonUtils.printThreadLog("here are not blocked,main continue");
CommonUtils.sleepSecond(4); // 此处休眠为的是等待CompletableFuture背后的线程池执行完成。
CommonUtils.printThreadLog("main end");
}
supplyAsync#
CompletableFuture.runAsync()
开启不带返回结果异步任务。但是,如果您想从后台的异步任务中返回一个结果怎么办?此时,CompletableFuture.supplyAsync()
是你最好的选择了。
static CompletableFuture<U> supplyAsync(Supplier<U> supplier)
它入参一个 Supplier 供给者,用于供给带返回值的异步任务
并返回CompletableFuture<U>
,其中U是供给者给程序供给值的类型。
需求:开启异步任务读取 news.txt 文件中的新闻稿,返回文件中内容并在主线程打印输出
fun main() {
CommonUtils.printThreadLog("main start")
val newsFuture = CompletableFuture.supplyAsync {
CommonUtils.readFile("news.txt")
}
CommonUtils.printThreadLog("here are not blocked, main continue")
val news = newsFuture.get()
CommonUtils.printThreadLog("news=$news")
CommonUtils.printThreadLog("main end")
}
如果想要获取newsFuture结果,可以调用completableFuture.get()方法,get()方法将阻塞,直到newsFuture完成。
异步任务中的线程池#
我们已经知道,runAsync()
和supplyAsync()
方法都是开启单独的线程中执行异步任务。但是,我们从未创建线程对吗? 不是吗!
CompletableFuture 会从全局的ForkJoinPool.commonPool()
线程池获取线程来执行这些任务
当然,你也可以创建一个线程池,并将其传递给runAsync()
和supplyAsync()
方法,以使它们在从您指定的线程池获得的线程中执行任务。
CompletableFuture API中的所有方法都有两种变体,一种是接受传入的Executor
参数作为指定的线程池,而另一种则使用默认的线程池 (ForkJoinPool.commonPool()
) 。
// runAsync() 的重载方法
static CompletableFuture<Void> runAsync(Runnable runnable)
static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
// supplyAsync() 的重载方法
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
需求:指定线程池,开启异步任务读取 news.txt 中的新闻稿,返回文件中内容并在主线程打印输出
fun main() {
CommonUtils.printThreadLog("main start")
val executor = Executors.newFixedThreadPool(4)
val newsFuture = CompletableFuture.supplyAsync({
CommonUtils.printThreadLog("异步读取文件开始")
CommonUtils.readFile("news.txt")
},executor)
CommonUtils.printThreadLog("here not blocked main continue")
val news = newsFuture.get()
CommonUtils.printThreadLog("news=$news")
executor.shutdown()
CommonUtils.printThreadLog("main end")
}
异步任务回调#
CompletableFuture.get()
方法是阻塞的。调用时它会阻塞等待 直到这个Future完成,并在完成后返回结果。 但是,很多时候这不是我们想要的。
对于构建异步系统,我们应该能够将回调附加到CompletableFuture上,当这个Future完成时,该回调应自动被调用。 这样,我们就不必等待结果了,然后在Future的回调函数内编写完成Future之后需要执行的逻辑。 您可以使用thenApply()
,thenAccept()
和thenRun()
方法,它们可以把回调函数附加到CompletableFuture
thenApply#
使用 thenApply()
方法可以处理和转换CompletableFuture的结果。 它以Function<T,R>作为参数。 Function<T,R>是一个函数式接口,表示一个转换操作,它接受类型T的参数并产生类型R的结果
CompletableFuture<R> thenApply(Function<T,R> fn)
需求:异步读取 filter_words.txt 文件中的内容,读取完成后,把内容转换成数组( 敏感词数组 ),异步任务返回敏感词数组
fun main() {
CommonUtils.printThreadLog("main start")
val readFileFuture = CompletableFuture.supplyAsync {
CommonUtils.printThreadLog("读取filter_words文件")
CommonUtils.readFile("filter_words.txt")
}
val filterWordsFuture = readFileFuture.thenApply {
CommonUtils.printThreadLog("文件内容转换成敏感词数组")
it.split(",")
}
val filterWords = filterWordsFuture.get()
CommonUtils.printThreadLog("main continue")
CommonUtils.printThreadLog("filterWords=${filterWords}")
}
你还可以通过附加一系列thenApply()
回调方法,在CompletableFuture上编写一系列转换序列。一个thenApply()
方法的结果可以传递给序列中的下一个,如果你对链式操作很了解,你会发现结果可以在链式操作上传递。
fun main() {
CommonUtils.printThreadLog("main start")
val result = CompletableFuture.supplyAsync {
CommonUtils.printThreadLog("读取filter_words文件")
CommonUtils.readFile("filter_words.txt")
}.thenApply {
CommonUtils.printThreadLog("文件内容转换成敏感词数组")
it.split(",")
}
CommonUtils.printThreadLog("${result.get()}")
}
thenAccept#
如果你不想从回调函数返回结果,而只想在Future完成后运行一些代码,则可以使用thenAccept()
这些方法是入参一个 Consumer
CompletableFuture<Void> thenAccept(Consumer<T> action)
通常用作回调链中的最后一个回调。
需求:异步读取 filter_words.txt 文件中的内容,读取完成后,转换成敏感词数组,然后打印敏感词数组
fun main() {
CommonUtils.printThreadLog("main start")
CompletableFuture.supplyAsync {
CommonUtils.printThreadLog("读取filter_words文件")
CommonUtils.readFile("filter_words.txt")
}.thenApply {
CommonUtils.printThreadLog("文件内容转换成敏感词数组")
it.split(",")
}.thenAccept { CommonUtils.printThreadLog("$it") }
CommonUtils.printThreadLog("main continue")
CommonUtils.sleepSecond(4)
CommonUtils.printThreadLog("main end")
}
thenRun#
前面我们已经知道,通过thenApply( Function<T,R> ) 对链式操作中的上一个异步任务的结果进行转换,返回一个新的结果;
通过thenAccept( Consumer
如果我们只是想从CompletableFuture的链式操作得到一个完成的通知,甚至都不使用上一步链式操作的结果,那么 CompletableFuture.thenRun() 会是你最佳的选择,它需要一个Runnable并返回CompletableFuture<Void>
。
CompletableFuture<Void> thenRun(Runnable action);
演示案例:我们仅仅想知道 filter_words.txt 的文件是否读取完成
fun main() {
printThreadLog("main start")
CompletableFuture.supplyAsync {
printThreadLog("读取filter_words文件")
val filterWordsContent = readFile("filter_words.txt")
filterWordsContent
}.thenRun { printThreadLog("读取filter_words文件读取完成") }
printThreadLog("main continue")
sleepSecond(4)
printThreadLog("main end")
}
更进一步提升并行化#
CompletableFuture 提供的所有回调方法都有两个异步变体
CompletableFuture<U> thenApply(Function<T,U> fn)
// 回调方法的异步变体(异步回调)
CompletableFuture<U> thenApplyAsync(Function<T,U> fn)
CompletableFuture<U> thenApplyAsync(Function<T,U> fn, Executor executor)
注意:这些带了Async的异步回调 通过在单独的线程中执行回调任务 来帮助您进一步促进并行化计算。
回顾需求:异步读取 filter_words.txt 文件中的内容,读取完成后,转换成敏感词数组,主线程获取结果打印输出这个数组
fun main() {
printThreadLog("main start")
val filterWordFuture = CompletableFuture.supplyAsync { "尼玛, NB, tmd" }.thenApply {
/**
* 一般而言,thenApply任务的执行和supplyAsync()任务执行可以使用同一线程执行
* 如果supplyAsync()任务立即返回结果,则thenApply的任务在主线程中执行
*/
printThreadLog("把内容转换成敏感词数组")
it.split(",")
}
printThreadLog("main continue")
val filterWords = filterWordFuture.get()
printThreadLog("filterWords = $filterWords")
printThreadLog("main end")
}
一般而言,thenApply任务的执行和supplyAsync()任务执行可以使用同一线程执行,如果supplyAsync()任务立即返回结果,则thenApply的任务在主线程中执行
要更好地控制执行回调任务的线程,可以使用异步回调。如果使用thenApplyAsync()
回调,那么它将在从ForkJoinPool.commonPool()
获得的另一个线程中执行
fun main() {
CommonUtils.printThreadLog("main start")
val filterWordFuture = CompletableFuture.supplyAsync {
// CommonUtils.printThreadLog("读取filter_words文件")
// val filterWordsContent = CommonUtils.readFile("filter_words.txt")
// filterWordsContent
"尼玛, NB, tmd"
}.thenApplyAsync {
CommonUtils.printThreadLog("把内容转换成敏感词数组")
it.split(",")
}
CommonUtils.printThreadLog("main continue")
val filterWords = filterWordFuture.get()
CommonUtils.printThreadLog("filterWords = $filterWords" )
CommonUtils.printThreadLog("main end")
}
此外,如果将Executor传递给thenApplyAsync()
回调,则该回调的异步任务将在从Executor的线程池中获取的线程中执行;
其他两个回调的变体版本如下:
// thenAccept和其异步回调
CompletableFuture<Void> thenAccept(Consumer<T> action)
CompletableFuture<Void> thenAcceptAsync(Consumer<T> action)
CompletableFuture<Void> thenAcceptAsync(Consumer<T> action, Executor executor)
// thenRun和其异步回调
CompletableFuture<Void> thenRun(Runnable action)
CompletableFuture<Void> thenRunAsync(Runnable action)
CompletableFuture<Void> thenRunAsync(Runnable action, Executor executor)
异步任务编排#
编排2个依赖关系的异步任务 thenCompose()#
回顾需求:异步读取 filter_words.txt 文件中的内容,读取完成后,转换成敏感词数组让主线程待用。
关于读取和解析内容,假设使用以下的 readFileFuture(String) 和 splitFuture(String) 方法完成。
fun readFileFuture(fileName:String):CompletableFuture<String>{
return CompletableFuture.supplyAsync{
CommonUtils.readFile(fileName)
}
}
fun splitFuture(context: String): CompletableFuture<List<String>> {
return CompletableFuture.supplyAsync {
context.split(",")
}
}
现在,让我们先了解如果使用thenApply()
结果会发生什么
val result:CompletableFuture<CompletableFuture<List<String>>> = readFileFuture("filter_words.txt")
.thenApply {
splitFuture(it)
}
CommonUtils.printThreadLog("result=${result.get().get()}")
}
回顾在之前的案例中,thenApply(Function<T,R>)
中Function回调会对上一步任务结果转换后得到一个简单值 ,但现在这种情况下,最终结果是嵌套的CompletableFuture,所以这是不符合预期的,那怎么办呢?
我们想要的是:把上一步异步任务的结果,转成一个CompletableFuture对象,这个CompletableFuture对象中包含本次异步任务处理后的结果。也就是说,我们想组合上一步异步任务的结果到下一个新的异步任务中, 结果由这个新的异步任务返回
此时,你需要使用thenCompose()
方法代替,我们可以把它理解为 异步任务的组合
CompletableFuture<R> thenCompose(Function<T,CompletableFuture<R>> func)
所以,thenCompose()
用来连接两个有依赖关系的异步任务,结果由第二个任务返回
因此,这里积累了一个经验:
如果我们想连接( 编排 ) 两个依赖关系的异步任务( CompletableFuture 对象 ) ,请使用 thenCompose() 方法
当然,thenCompose 也存在异步回调变体版本:
CompletableFuture<R> thenCompose(Function<T,CompletableFuture<R>> fn)
CompletableFuture<R> thenComposeAsync(Function<T,CompletableFuture<R>> fn)
CompletableFuture<R> thenComposeAsync(Function<T,CompletableFuture<R>> fn, Executor executor)
编排2个非依赖关系的异步任务 thenCombine()
我们已经知道,当其中一个Future依赖于另一个Future,使用thenCompose()
用于组合两个Future。如果两个Future之间没有依赖关系,你希望两个Future独立运行并在两者都完成之后执行回调操作时,则使用thenCombine()
;
// T是第一个任务的结果 U是第二个任务的结果 V是经BiFunction应用转换后的结果
CompletableFuture<V> thenCombine(CompletableFuture<U> other, BiFunction<T,U,V> func)
需求:替换新闻稿 ( news.txt ) 中敏感词汇 ,把敏感词汇替换成*,敏感词存储在 filter_words.txt 中
fun main() {
val future1 = CompletableFuture.supplyAsync {
CommonUtils.printThreadLog("读取敏感词汇并解析")
val context = CommonUtils.readFile("filter_words.txt")
context.split(",")
}
val future2 = CompletableFuture.supplyAsync{
CommonUtils.printThreadLog("读取news文件内容")
CommonUtils.readFile("news.txt")
}
val combinedFuture = future1.thenCombine(future2){words,context->
// 替换操作
var context = context
CommonUtils.printThreadLog("替换操作");
for (word in words) {
if (context.indexOf(word)>-1) {
context = context.replace(word,"**")
}
}
context
}
CommonUtils.printThreadLog("filteredContext=${combinedFuture.get()}")
}
注意:当两个Future
都完成时,才将两个异步任务的结果传递给thenCombine()
的回调函数做进一步处理。
和以往一样,thenCombine 也存在异步回调变体版本
CompletableFuture<V> thenCombine(CompletableFuture<U> other, BiFunction<T,U,V> func)
CompletableFuture<V> thenCombineAsync(CompletableFuture<U> other, BiFunction<T,U,V> func)
CompletableFuture<V> thenCombineAsync(CompletableFuture<U> other, BiFunction<T,U,V> func,Executor executor)
合并多个异步任务 allOf / anyOf#
我们使用thenCompose()
和thenCombine()
将两个CompletableFuture组合和合并在一起。
如果要编排任意数量的CompletableFuture怎么办?可以使用以下方法来组合任意数量的CompletableFuture
public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)
CompletableFuture.allOf()
用于以下情形中:有多个需要独立并行运行的Future
,并在所有这些Future
都完成后执行一些操作。
需求:统计news1.txt、new2.txt、new3.txt 文件中包含CompletableFuture关键字的文件的个数
fun main() {
// step 1: 创建List集合存储文件名
val fileList = listOf("news1.txt", "news2.txt", "news3.txt")
// step 2: 根据文件名调用readFileFuture创建多个CompletableFuture,并存入List集合中
val readFileFutureList = fileList.map {
readFileFuture(it)
}.toList()
// step 3: 把List集合转换成数组待用,以便传入allOf方法中
val array = readFileFutureList.toTypedArray()
// step 4: 使用allOf方法合并多个异步任务
val allOfFuture = CompletableFuture.allOf(*array)
// step 5: 当多个异步任务都完成后,使用回调操作文件结果,统计符合条件的文件个数
val countFuture = allOfFuture.thenApply { _ ->
readFileFutureList.map { future -> future.join() }
.count { content -> content.contains("CompletableFuture") }
}
// step 6: 主线程打印输出文件个数
val count = countFuture.join()
CommonUtils.printThreadLog("count=$count")
}
private fun readFileFuture(fileName:String):CompletableFuture<String>{
return CompletableFuture.supplyAsync{
CommonUtils.readFile(fileName)
}
}
顾名思义,当给定的多个异步任务中的有任意Future一个完成时,需要执行一些操作,可以使用 anyOf 方法
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)
anyOf()
返回一个新的CompletableFuture,新的CompletableFuture的结果和 cfs中已完成的那个异步任务结果相同。
演示案例:anyOf 执行过程
fun main() {
val future1 = CompletableFuture.supplyAsync {
CommonUtils.sleepSecond(1)
"Future1的结果"
}
val future2 = CompletableFuture.supplyAsync {
CommonUtils.sleepSecond(2)
"Future2的结果"
}
val future3 = CompletableFuture.supplyAsync {
CommonUtils.sleepSecond(3)
"Future3的结果"
}
val anyOfFuture = CompletableFuture.anyOf(future1, future2, future3)
CommonUtils.printThreadLog("anyOfFuture=${anyOfFuture.get()}")
}
在上面的示例中,当三个CompletableFuture中的任意一个完成时,anyOfFuture就完成了。 由于future2的睡眠时间最少,因此它将首先完成,最终结果将是"Future2的结果"。
注意:
anyOf()
方法返回类型必须是CompletableFuture <Object>
。anyOf()
的问题在于,如果您拥有返回不同类型结果的CompletableFuture,那么您将不知道最终CompletableFuture的类型
异步任务的异常处理#
在前面的章节中,我们并没有更多地关心异常处理的问题,其实,CompletableFuture 提供了优化处理异常的方式。
首先,让我们了解异常如何在回调链中传播。
fun main() {
CompletableFuture.supplyAsync{
val r = 1/0
"result"
}.thenApply { result->
"$result result2"
}.thenApply {result2->
"$result2 result3"
}.thenApply { result3->
CommonUtils.printThreadLog(result3)
}
}
如果在 supplyAsync 任务中出现异常,后续的 thenApply 和 thenAccept 回调都不会执行,CompletableFuture 将转入异常处理
如果在第一个 thenApply 任务中出现异常,第二个 thenApply 和 最后的 thenAccept 回调不会被执行,CompletableFuture 将转入异常处理,依次类推。
exceptionally()#
exceptionally 用于处理回调链上的异常,回调链上出现的任何异常,回调链不继续向下执行,都在exceptionally中处理异常。
// Throwable表示具体的异常对象e
CompletableFuture<R> exceptionally(Function<Throwable, R> func)
fun main() {
val future = CompletableFuture.supplyAsync{
val r = 1/0
"result"
}.thenApply { result->
val str :String?= null
val len = str!!.length
"$result result2"
}.thenApply {result2->
"$result2 result3"
}.thenApply { result3->
CommonUtils.printThreadLog(result3)
}.exceptionally { ex->
CommonUtils.printThreadLog("出现异常:${ex.message}")
}
val ret = future.get()
CommonUtils.printThreadLog("最终结果:$ret")
}
因为exceptionally只处理一次异常,所以常常用在回调链的末端。
handle()#
CompletableFuture API 还提供了一种更通用的方法 handle()
表示从异常中恢复
handle() 常常被用来恢复回调链中的一次特定的异常,回调链恢复后可以进一步向下传递。
CompletableFuture<R> handle(BiFunction<T, Throwable, R> fn)
fun main() {
val future = CompletableFuture.supplyAsync {
val r = 1/0
"result"
}.handle { ret, ex ->
if (ex!=null){
CommonUtils.printThreadLog("我们得到异常:${ex.message}")
"Unknow!"
}
ret
}
CommonUtils.printThreadLog(future.get())
}
如果发生异常,则res参数将为null,否则ex参数将为null。
需求:对回调链中的一次异常进行恢复处理
fun main() {
val future = CompletableFuture.supplyAsync {
val r = 1/0
"result1"
}.handle { ret, ex ->
if (ex!=null){
CommonUtils.printThreadLog("我们得到异常:${ex.message}")
"Unknow!"
}
ret
}.thenApply { result->
val str:String?=null
val len = str!!.length
"$result result2"
}.handle { ret, ex ->
if (ex!=null){
CommonUtils.printThreadLog("我们得到异常:${ex.message}")
"Unknow!"
}
ret
}.thenApply { result->
"$result result3"
}
val ret = future.get()
CommonUtils.printThreadLog("最终结果:$ret")
}
和以往一样,为了提升并行化,异常处理可以方法单独的线程执行,以下是它们的异步回调版本
CompletableFuture<R> exceptionally(Function<Throwable, R> fn)
CompletableFuture<R> exceptionallyAsync(Function<Throwable, R> fn) // jdk17+
CompletableFuture<R> exceptionallyAsync(Function<Throwable, R> fn,Executor executor) // jdk17+
CompletableFuture<R> handle(BiFunction<T,Throwable,R> fn)
CompletableFuture<R> handleAsync(BiFunction<T,Throwable,R> fn)
CompletableFuture<R> handleAsync(BiFunction<T,Throwable,R> fn, Executor executor)
异步任务的交互#
异步任务交互指将异步任务获取结果的速度相比较,按一定的规则( 先到先用 )进行下一步处理。
applyToEither#
applyToEither()
把两个异步任务做比较,异步任务先到结果的,就对先到的结果进行下一步的操作。
CompletableFuture<R> applyToEither(CompletableFuture<T> other, Function<T,R> func)
演示案例:使用最先完成的异步任务的结果
fun main() {
// 开启异步任务1
val future1 = CompletableFuture.supplyAsync {
val x = Random().nextInt(3)
CommonUtils.sleepSecond(x)
CommonUtils.printThreadLog("任务1耗时:$x 秒")
x
}
// 开启异步任务2
val future2 = CompletableFuture.supplyAsync {
val x = Random().nextInt(3)
CommonUtils.sleepSecond(x)
CommonUtils.printThreadLog("任务2耗时:$x 秒")
x
}
// 哪些异步任务的结果先到达,就使用哪个异步任务的结果
val future = future1.applyToEither(future2){
CommonUtils.printThreadLog("最先到达的结果:$it")
it
}
// 主线程休眠4秒,等待所有异步任务完成
CommonUtils.sleepSecond(4)
CommonUtils.printThreadLog("ret= ${future.get()}")
}
以下是applyToEither 和其对应的异步回调版本
CompletableFuture<R> applyToEither(CompletableFuture<T> other, Function<T,R> func)
CompletableFuture<R> applyToEitherAsync(CompletableFuture<T> other, Function<T,R> func)
CompletableFuture<R> applyToEitherAsync(CompletableFuture<T> other, Function<T,R> func,Executor executor)
acceptEither#
acceptEither()
把两个异步任务做比较,异步任务先到结果的,就对先到的结果进行下一步操作 ( 消费使用 )。
CompletableFuture<Void> acceptEither(CompletableFuture<T> other, Consumer<T> action)
CompletableFuture<Void> acceptEitherAsync(CompletableFuture<T> other, Consumer<T> action)
CompletableFuture<Void> acceptEitherAsync(CompletableFuture<T> other, Consumer<T> action,Executor executor)
演示案例:使用最先完成的异步任务的结果
fun main() {
// 异步任务交互
CommonUtils.printThreadLog("main start");
// 开启异步任务1
val future1 = CompletableFuture.supplyAsync {
val x = Random().nextInt(3)
CommonUtils.sleepSecond(x)
CommonUtils.printThreadLog("任务1耗时: $x 秒");
}
// 开启异步任务2
val future2 = CompletableFuture.supplyAsync {
val x = Random().nextInt(3)
CommonUtils.sleepSecond(x)
CommonUtils.printThreadLog("任务2耗时: $x 秒");
}
future1.acceptEither(future2){
CommonUtils.printThreadLog("最先到达的结果:$it")
}
// 主线程休眠4秒,等待所有异步任务完成
CommonUtils.sleepSecond(4);
CommonUtils.printThreadLog("main end");
}
如果不关心最先到达的结果,只想在有一个异步任务先完成时得到完成的通知,可以使用 runAfterEither()
,以下是它的相关方法:
CompletableFuture<Void> runAfterEither(CompletableFuture<T> other, Runnable action)
CompletableFuture<Void> runAfterEitherAsync(CompletableFuture<T> other, Runnable action)
CompletableFuture<Void> runAfterEitherAsync(CompletableFuture<T> other, Runnable action, Executor executor)
get() 和 join() 区别#
get() 和 join() 都是CompletableFuture提供的以阻塞方式获取结果的方法。
那么该如何选用呢?请看如下案例:
fun main() {
val future = CompletableFuture.supplyAsync {
"hello"
}
var ret:String? = null
// 抛出检查时异常,必须处理
try {
ret = future.get()
}catch (e:InterruptedException){
e.printStackTrace()
}catch (e:ExecutionException){
e.printStackTrace()
}
CommonUtils.printThreadLog("ret=$ret")
// 抛出运行时异常,可以不处理
ret = future.join()
CommonUtils.printThreadLog("ret=$ret")
}
使用时,我们发现,get() 抛出检查时异常 ,需要程序必须处理;而join() 方法抛出运行时异常,程序可以不处理。所以,join() 更适合用在流式编程中。
ParallelStream VS CompletableFuture#
CompletableFuture 虽然提高了任务并行处理的能力,如果它和 Stream API 结合使用,能否进一步多个任务的并行处理能力呢?
同时,对于 Stream API 本身就提供了并行流ParallelStream,它们有什么不同呢?
我们将通过一个耗时的任务来体现它们的不同,更重要地是,我们能进一步加强 CompletableFuture 和 Stream API 的结合使用,同时搞清楚CompletableFuture 在流式操作的优势
class MyTask (val duration:Int){
// 模拟耗时的长任务
fun doWork():Int{
CommonUtils.printThreadLog("doWork")
CommonUtils.sleepSecond(duration)
return duration
}
}
fun main() {
val tasks = IntStream.range(0, 10).mapToObj {
MyTask(1)
}.toList()
val start = System.currentTimeMillis()
val result = tasks.map {
it.doWork()
}.toList()
val end = System.currentTimeMillis()
val costTime = (end-start)/1000.0
CommonUtils.printThreadLog("processed ${tasks.size} cost second $costTime")
}
它花费了10秒, 因为每个任务在主线程一个接一个的执行。
因为涉及 Stream API,而且存在耗时的长任务,所以,我们可以使用 parallelStream()
fun main() {
val tasks = IntStream.range(0, 10).mapToObj {
MyTask(1)
}.toList()
val start = System.currentTimeMillis()
val result = tasks.parallelStream().map {
it.doWork()
}.toList()
val end = System.currentTimeMillis()
val costTime = (end-start)/1000.0
CommonUtils.printThreadLog("processed ${tasks.size} cost second $costTime")
}
它花费了2秒多,因为此次并行执行使用了10个线程 (9个是ForkJoinPool线程池中的, 一个是 main 线程),需要注意是:运行结果由自己电脑CPU的核数决定
CompletableFuture 在流式操作的优势#
让我们看看使用CompletableFuture是否执行的更有效率
fun main() {
val tasks = IntStream.range(0, 10).mapToObj {
MyTask(1)
}.toList()
val start = System.currentTimeMillis()
val futures = tasks.map { myTask->
CompletableFuture.supplyAsync {
myTask.doWork()
}
}.toList()
val results = futures.map { it.join() }
val end = System.currentTimeMillis()
val costTime = (end-start)/1000.0
CommonUtils.printThreadLog("processed ${tasks.size} cost second $costTime")
}
运行发现,两者使用的时间大致一样。能否进一步优化呢?
CompletableFutures 比 ParallelStream 优点之一是你可以指定Executor去处理任务。你能选择更合适数量的线程。我们可以选择大于Runtime.getRuntime().availableProcessors() 数量的线程,如下所示:
fun main() {
val tasks = IntStream.range(0, 10).mapToObj {
MyTask(1)
}.toList()
val CPU_NUM = Runtime.getRuntime().availableProcessors()
val executor = Executors.newFixedThreadPool(Math.max(tasks.size,CPU_NUM))
val start = System.currentTimeMillis()
val futures = tasks.map { myTask->
CompletableFuture.supplyAsync( {
myTask.doWork()
},executor)
}.toList()
val results = futures.map { it.join() }
val end = System.currentTimeMillis()
val costTime = (end-start)/1000.0
CommonUtils.printThreadLog("processed ${tasks.size} cost second $costTime")
executor.shutdown()
}
合理配置线程池中的线程数#
正如我们看到的,CompletableFuture 可以更好地控制线程池中线程的数量,而 ParallelStream 不能。
问题1:如何选用 CompletableFuture 和 ParallelStream ?
如果你的任务是IO密集型的,你应该使用CompletableFuture;
如果你的任务是CPU密集型的,使用比处理器更多的线程是没有意义的,所以选择ParallelStream ,因为它不需要创建线程池,更容易使用。
问题2:IO密集型任务和CPU密集型任务的区别?
CPU密集型也叫计算密集型,此时,系统运行时大部分的状况是CPU占用率近乎100%,I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU 使用率很高。比如说要计算1+2+3+…+ 10万亿、天文计算、圆周率后几十位等, 都是属于CPU密集型程序。
CPU密集型任务的特点:大量计算,CPU占用率一般都很高,I/O时间很短
IO密集型指大部分的状况是CPU在等I/O (硬盘/内存) 的读写操作,但CPU的使用率不高。
简单的说,就是需要大量的输入输出,例如读写文件、传输文件、网络请求。
IO密集型任务的特点:大量网络请求,文件操作,CPU运算少,很多时候CPU在等待资源才能进一步操作。
问题3:既然要控制线程池中线程的数量,多少合适呢?
如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 Ncpu+1
如果是IO密集型任务,参考值可以设置为 2 * Ncpu,其中Ncpu 表示 核心数。
注意的是:以上给的是参考值,详细配置超出本次课程的范围,选不赘述。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 上周热点回顾(2.17-2.23)
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)