CompletableFuture从入门到精通?算了,入个门就行了

LOVELETTERD·2023-04-07 22:43·104 次阅读

CompletableFuture从入门到精通?算了,入个门就行了

Future vs CompletableFuture#

准备工作#

为了便于后续更好地调试和学习,我们需要定义一个工具类CommonUtils辅助我们对知识的理解。这个工具类总共四个方法

  • readFile:读取指定路径的文件内容
  • sleepMillis:休眠指定的毫秒数
  • sleepSecond:休眠指定的秒数
  • printThreadLog:打印携带线程信息的日志信息
Copy
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

Copy
oh my god!completablefuture真tmd好用

filter_words.txt

Copy
尼玛,SB,tmd
Copy
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 实现了FutureCompletionStage接口

CompletableFuture 相对于 Future 具有以下优势:

  • 为快速创建、链接依赖和组合多个Future提供了大量的便利方法。
  • 提供了适用于各种开发场景的回调函数,它还提供了非常全面的异常处理支持。
  • 无缝衔接和亲和 lambda 表达式 和 Stream - API 。
  • 我见过的真正意义上的异步编程,把异步编程和函数式编程、响应式编程多种高阶编程思维集于一身,设计上更优雅。

创建异步任务#

runAsync#

如果你要异步运行某些耗时的后台任务,并且不想从任务中返回任何内容,则可以使用CompletableFuture.runAsync()方法。它接受一个Runnable接口的实现类对象,方法返回CompletableFuture<Void> 对象

Copy
static CompletableFuture<Void> runAsync(Runnable runnable);

演示案例:开启一个不从任务中返回任何内容的CompletableFuture异步任务

Copy
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()是你最好的选择了。

Copy
static CompletableFuture<U> supplyAsync(Supplier<U> supplier)

它入参一个 Supplier 供给者,用于供给带返回值的异步任务
并返回CompletableFuture<U>,其中U是供给者给程序供给值的类型。

需求:开启异步任务读取 news.txt 文件中的新闻稿,返回文件中内容并在主线程打印输出

Copy
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() ) 。

Copy
// 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 中的新闻稿,返回文件中内容并在主线程打印输出

Copy
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的结果

Copy
CompletableFuture<R> thenApply(Function<T,R> fn)

需求:异步读取 filter_words.txt 文件中的内容,读取完成后,把内容转换成数组( 敏感词数组 ),异步任务返回敏感词数组

Copy
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()方法的结果可以传递给序列中的下一个,如果你对链式操作很了解,你会发现结果可以在链式操作上传递。

Copy
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

Copy
CompletableFuture<Void> thenAccept(Consumer<T> action)

通常用作回调链中的最后一个回调。

需求:异步读取 filter_words.txt 文件中的内容,读取完成后,转换成敏感词数组,然后打印敏感词数组

Copy
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>

Copy
CompletableFuture<Void> thenRun(Runnable action);

演示案例:我们仅仅想知道 filter_words.txt 的文件是否读取完成

Copy
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 提供的所有回调方法都有两个异步变体

Copy
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 文件中的内容,读取完成后,转换成敏感词数组,主线程获取结果打印输出这个数组

Copy
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() 获得的另一个线程中执行

Copy
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的线程池中获取的线程中执行;

Copy
其他两个回调的变体版本如下: // 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) 方法完成。

Copy
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() 结果会发生什么

Copy
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()方法代替,我们可以把它理解为 异步任务的组合

Copy
CompletableFuture<R> thenCompose(Function<T,CompletableFuture<R>> func)

所以,thenCompose()用来连接两个有依赖关系的异步任务,结果由第二个任务返回

因此,这里积累了一个经验:

如果我们想连接( 编排 ) 两个依赖关系的异步任务( CompletableFuture 对象 ) ,请使用 thenCompose() 方法

当然,thenCompose 也存在异步回调变体版本:

Copy
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();

Copy
// T是第一个任务的结果 U是第二个任务的结果 V是经BiFunction应用转换后的结果 CompletableFuture<V> thenCombine(CompletableFuture<U> other, BiFunction<T,U,V> func)

需求:替换新闻稿 ( news.txt ) 中敏感词汇 ,把敏感词汇替换成*,敏感词存储在 filter_words.txt 中

Copy
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 也存在异步回调变体版本

Copy
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

Copy
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关键字的文件的个数

Copy
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 方法

Copy
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)

anyOf()返回一个新的CompletableFuture,新的CompletableFuture的结果和 cfs中已完成的那个异步任务结果相同。

演示案例:anyOf 执行过程

Copy
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 提供了优化处理异常的方式。

首先,让我们了解异常如何在回调链中传播

Copy
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中处理异常。

Copy
// Throwable表示具体的异常对象e CompletableFuture<R> exceptionally(Function<Throwable, R> func)
Copy
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() 常常被用来恢复回调链中的一次特定的异常,回调链恢复后可以进一步向下传递。

Copy
CompletableFuture<R> handle(BiFunction<T, Throwable, R> fn)
Copy
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。

需求:对回调链中的一次异常进行恢复处理

Copy
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") }

和以往一样,为了提升并行化,异常处理可以方法单独的线程执行,以下是它们的异步回调版本

Copy
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() 把两个异步任务做比较,异步任务先到结果的,就对先到的结果进行下一步的操作。

Copy
CompletableFuture<R> applyToEither(CompletableFuture<T> other, Function<T,R> func)

演示案例:使用最先完成的异步任务的结果

Copy
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 和其对应的异步回调版本

Copy
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() 把两个异步任务做比较,异步任务先到结果的,就对先到的结果进行下一步操作 ( 消费使用 )。

Copy
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)

演示案例:使用最先完成的异步任务的结果

Copy
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() ,以下是它的相关方法:

Copy
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提供的以阻塞方式获取结果的方法。

那么该如何选用呢?请看如下案例:

Copy
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 在流式操作的优势

Copy
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()

Copy
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是否执行的更有效率

Copy
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() 数量的线程,如下所示:

Copy
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 表示 核心数。

注意的是:以上给的是参考值,详细配置超出本次课程的范围,选不赘述。

posted @   loveletters  阅读(105)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 上周热点回顾(2.17-2.23)
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
点击右上角即可分享
微信分享提示
目录