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 实现了FutureCompletionStage接口

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

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

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

posted @ 2023-04-07 22:43  loveletters  阅读(79)  评论(1编辑  收藏  举报