CompletableFuture入门

学习目标

  • 了解CompletableFuture的优点
  • 掌握创建异步任务
    • 创建异步任务的2种方式
    • 知道异步任务中线程池的作用
    • 理解异步编程思想
  • 掌握异步任务回调
    • thenApply/thenAccept/thenRun 3类方法使用和区别
    • 解锁一系列Async版本回调(thenXxxAsync)
  • 掌握异步任务编排
    • 会对2个异步任务的依赖关系、并行关系进行编排
    • 会对n个任务的合并进行编排
  • 掌握异步任务的异常处理
    • 会对异步任务进行异常处理
    • 会对回调链上单个异步任务的异常进行现场恢复

1. Future VS CompletableFuture

1.1 准备工作

定义辅助工具类

public class CommonUtils {

    // 读取指定路径的文件
    public static String readFile(String pathToFile) {
        try {
            return Files.readString(Paths.get(pathToFile));
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }
    }

    // 休眠指定的毫秒数
    public static void sleepMillis(long millis) {
        try {
            TimeUnit.MILLISECONDS.sleep(millis);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 休眠指定的分钟数
    public static void sleepSecond(int seconds) {
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static String getCurrentTime() {
        LocalTime now = LocalTime.now();
        return now.format(DateTimeFormatter.ofPattern("HH:mm:ss.SSS"));
    }

    // 打印携带线程信息的日志
    public static void printThreadLog(String message) {
        // 时间戳-线程id-线程名-日志信息
        String result = new StringJoiner(" - ").add(getCurrentTime()).add(String.format("%2d", Thread.currentThread().getId())).add(Thread.currentThread().getName()).add(message).toString();
        System.out.println(result);
    }
}

filter_words.txt

尼玛,NB,tmd

news.txt

异步多线程:CompletableFuture真tmd好用

news1.txt

异步多线程:CompletableFuture真tmd好用

news2.txt

CompletableFuture真tmd好用

news3.txt

omgd:CompletableFuture真tmd好用

1.2 Future的局限性

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

public class FutureDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(5);
        // step1: 读取敏感词汇
        Future<String[]> filterWordFuture = executor.submit(() -> {
            String str = CommonUtils.readFile("D:\\JavaWorkSpace\\code\\arithmetic\\algorithm\\src\\com\\zet\\asyn_thread\\filter_words.txt");
            String[] filterWords = str.split(",");
            return filterWords;
        });
        // step2: 读取新闻稿
        Future<String> newsFuture = executor.submit(() -> CommonUtils.readFile("D:\\JavaWorkSpace\\code\\arithmetic\\algorithm\\src\\com\\zet\\asyn_thread\\news.txt"));
        // step3: 替换操作
        Future<String> replaceFuture = executor.submit(() -> {
            String[] filterWords = filterWordFuture.get();
            String news = newsFuture.get();
            for (String filterWord : filterWords) {
                if (news.indexOf(filterWord) >= 0) {
                    news = news.replace(filterWord, "**");
                }
            }
            return news;
        });
        // step4: 打印输出替换后的新闻稿
        String filteredNews = replaceFuture.get();
        System.out.println(filteredNews);
        executor.shutdown();
    }
}

运行结果

异步多线程:CompletableFuture**好用

通过上面的代码,我们会发现,Future相比于所有任务都直接在主线程处理,有很多优势,但同时也存在不足

  • 在没有阻塞的情况下,无法对Future的结果执行进一步的操作。Future不会告知你它什么时候完成,你如果想要知道结果,必须通过get() 方法获取,该方法会阻塞直到结果可用为止。它不具备将回调函数附加到Future后并在Future的结果可用时自动调用回调的能力。
  • 无法解决任务相互依赖的问题。filterWordFuture和newsFuture的结果不能自动发送给replaceFuture,需要在replaceFuture中手动获取,所以使用Future不能轻而易举的创建异步任务。
  • 不能将多个Future合并在一起。假设你有多种不同的Future,你想在它们全部并行完成然后再运行某个函数,Future很难独立完成这一需要。
  • 没有异常处理、Future提供的方法中没有专门的API应对异常处理,还是需要开发者自己手动异常处理。

1.3 CompletableFuture的优势

在这里插入图片描述

CompletableFuture实现了Future和CompletableStage接口
CompletableFuture相对于 Future 具有以下优势:

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

2.创建异步任务

2.1 runAsync

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

static CompletableFuture<Void> runAsync(Runnable runnable);

演示示例:创建一个不从任务中返回任何内容的CompletableFuture异步任务对象

public class RunAsyncDemo1 {
    public static void main(String[] args) {
        CommonUtils.printThreadLog("main start");
        // runAsync 创建异步任务,没有返回值
        CompletableFuture.runAsync(new Runnable() {
            @Override
            public void run() {
                CommonUtils.printThreadLog("读取文件开始!");
                CommonUtils.sleepSecond(3);
                CommonUtils.printThreadLog("读取文件结束!");
            }
        });
        CommonUtils.printThreadLog("here are not blocked, main continue");
        CommonUtils.sleepSecond(4);
        CommonUtils.printThreadLog("main end");
    }
}

运行结果

21:04:31.882 -  1 - main - main start
21:04:31.897 -  1 - main - here are not blocked, main continue
21:04:31.897 - 14 - ForkJoinPool.commonPool-worker-3 - 读取文件开始!
21:04:34.906 - 14 - ForkJoinPool.commonPool-worker-3 - 读取文件结束!
21:04:35.909 -  1 - main - main end

2.2 supplyAsync

CompletableFuture.runAsync() 开启不带返回结果的异步任务。但是,如果想从后台的异步任务中返回一个结果怎么办?此时,CompletableFuture.supplyAsync() 是最好的选择

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

入参是一个Supplier<U> 供给者,用于供给带返回值的异步任务
并返回CompletableFuture<U>,其中U是供给者给程序供给值的类型
需求:开启异步任务读取 news.txt 文件中的新闻稿,返回文件中内容并在主线程打印输出

public class SupplyAsyncDemo1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 需求:开启异步任务读取news.txt文件中的新闻稿,返回文件中的内容并在主线程中打印
        CommonUtils.printThreadLog("main start");
        CompletableFuture<String> newsFuture = CompletableFuture.supplyAsync(new Supplier<String>() {
            @Override
            public String get() {
                String content = CommonUtils.readFile("D:\\JavaWorkSpace\\code\\arithmetic\\algorithm\\src\\com\\zet\\asyn_thread\\news.txt");
                return content;
            }
        });
        CommonUtils.printThreadLog("here not blocked main continue");
        String news = newsFuture.get();
        System.out.println("news = " + news);
        CommonUtils.printThreadLog("main end");

        /**
         * 疑问:get方法阻塞,会不会影响程序的性能?
         */
    }
}

运行结果

21:05:40.449 -  1 - main - main start
21:05:40.468 -  1 - main - here not blocked main continue
news = 异步多线程:CompletableFuture真tmd好用
21:05:40.478 -  1 - main - main end

2.3 异步任务中的线程池

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

public class SupplyAsyncDemo3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(4);
        // 需求:开启异步任务读取news.txt文件中的新闻稿,返回文件中的内容并在主线程中打印
        CommonUtils.printThreadLog("main start");
        CompletableFuture<String> newsFuture = CompletableFuture.supplyAsync(() -> {
            CommonUtils.printThreadLog("异步任务读取文件!");
            String content = CommonUtils.readFile("D:\\JavaWorkSpace\\code\\arithmetic\\algorithm\\src\\com\\zet\\asyn_thread\\news.txt");
            return content;
        }, executor);
        CommonUtils.printThreadLog("here not blocked main continue");
        String news = newsFuture.get();
        System.out.println("news = " + news);
        CommonUtils.printThreadLog("main end");
        // 关闭线程池
        executor.shutdown();
    }
}

运行结果

21:06:27.244 -  1 - main - main start
21:06:27.259 -  1 - main - here are blocked main continue
21:06:27.259 - 14 - ForkJoinPool.commonPool-worker-3 - 异步多线程:CompletableFuture真tmd好用
21:06:31.272 -  1 - main - main end

结论:如果所有CompletableFuture共享一个线程池,那么一旦有异步任务执行一些很慢的I/O操作,就会导致线程池中所有线程都阻塞在I/O操作上,从而造成线程饥饿,进而影响整个系统的性能。
所以,强烈建议根据不同的业务类型创建不同的线程池,以避免互相干扰。

2.4 异步编程思想

        异步编程是可以让程序并行(也可能是并发)运行的一种手段,其可以让程序中的一个工作单元作为异步任务与主线程分开独立运行,并且在异步任务运行结束后,会通知主线程它的运行结果或者失败原因,毫无疑问,一个异步任务其实就是开启一个线程来完成的,使用异步编程可以提高应用程序的性能和响应能力等。

作为开发者,只需要有一个意识:

开发者只需要把耗时的操作交给CompletableFuture开启一个异步任务,然后继续关注主线程业务,当异步任务运行完成时会通知主线程它的运行结果。我们把具备了这种编程思想的开发成为异步编程思想

3. 任务异步回调

CompletableFuture.get() 方法是阻塞的。调用时它会阻塞等待直到这个Future完成,并在完成后返回结果。但是,很多时候这个不是我们想要的。对于构建异步系统,我们应该能够将回调附加到CompletableFuture上,当这个Future完成时,该回调应自动被调用。这样,我们就不必等待结果了,我们可以在Future的回调函数内部编写完成Future之后需要执行的逻辑。可以使用thenApply(),thenAccept()和thenRun() 方法将回调函数附加到CompletableFuture

3.1 thenApply()

使用 thenApply() 方法可以处理和转换CompletableFuture的结果。它以Function<T, R>作为参数。Function<T, R>是一个函数式接口,表示一个转换操作,它接受类型T的参数并产生类型R的结果。

CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)

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

public class ThenApplyDemo1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 需求:异步读取filter_words.txt文件中的内容。读取完成后,把内容转换
        // 成数组(敏感词数组),异步任务返回敏感词数组
        CommonUtils.printThreadLog("main start");
        CompletableFuture<String> readFileFuture = CompletableFuture.supplyAsync(() -> {
            CommonUtils.printThreadLog("读取filter_words文件");
            String filterWordContent = CommonUtils.readFile("D:\\JavaWorkSpace\\code\\arithmetic\\algorithm\\src\\com\\zet\\asyn_thread\\filter_words.txt");
            return filterWordContent;
        });

        CompletableFuture<String[]> filterWordFuture = readFileFuture.thenApply(content -> {
            CommonUtils.printThreadLog("把文件内容转换为敏感词数组");
            String[] filterWords = content.split(",");
            return filterWords;
        });

        CommonUtils.printThreadLog("main continue");
        String[] filterWords = filterWordFuture.get();
        CommonUtils.printThreadLog("filterWords = " + Arrays.toString(filterWords));
        CommonUtils.printThreadLog("main end");

        /**
         * 总结:
         * thenApply(Function<T, R>) 可以将异步任务的结果进一步应用Function转换
         * 转换后的结果可以在主线程获取,也可以进行下一步的转换
         */
    }
}

运行结果

21:09:57.752 -  1 - main - main start
21:09:57.783 - 14 - ForkJoinPool.commonPool-worker-3 - 读取filter_words文件
21:09:57.783 -  1 - main - main continue
21:09:57.783 - 14 - ForkJoinPool.commonPool-worker-3 - 把文件内容转换为敏感词数组
21:09:57.783 -  1 - main - filterWords = [尼玛, NB, tmd]
21:09:57.783 -  1 - main - main end

3.2 thenAccept()

如果你不想从回调函数返回结果,而只想在Future完成后运行一些代码,则可以使用thenAccept()
入参是一个Consumer<T>,它可以对异步任务的执行结果进行消费使用,方法返回CompletableFuture<Void>

CompletableFuture<Void> thenAccept(Consumer<? super T> action)

通常用作回调链中的最后一个回调。
需求:异步读取 filter_words.txt 文件中的内容,读取完成后,转换成敏感词汇数组,然后打印敏感词数组。

public class ThenAcceptDemo {
    public static void main(String[] args) {
        // 需求:异步读取 filter_words.txt 文件中的内容,读取完成后,转换成敏感词数组,然后打印敏感词数组
        CommonUtils.printThreadLog("main start");

        CompletableFuture.supplyAsync(() -> {
            CommonUtils.printThreadLog("读取filter_words文件");
            String filterWordsContent = CommonUtils.readFile("D:\\JavaWorkSpace\\code\\arithmetic\\algorithm\\src\\com\\zet\\asyn_thread\\filter_words.txt");
            return filterWordsContent;
        }).thenApply(content -> {
            CommonUtils.printThreadLog("把文件内容转换成敏感词数组");
            String[] filterWords = content.split(",");
            return filterWords;
        }).thenAccept(filterWords -> {
            CommonUtils.printThreadLog("filterWords = " + Arrays.toString(filterWords));
        });

        CommonUtils.printThreadLog("main continue");
        CommonUtils.sleepSecond(4);
        CommonUtils.printThreadLog("main end");

        /**
         * 总结:
         * thenAccept(Consumer<T> c) 可以对异步任务的结果进行消费使用
         * 返回一个不带结果的CompletableFuture对象
         *
         */
    }
}

运行结果

21:11:03.282 -  1 - main - main start
21:11:03.319 - 14 - ForkJoinPool.commonPool-worker-3 - 读取filter_words文件
21:11:03.319 -  1 - main - main continue
21:11:03.319 - 14 - ForkJoinPool.commonPool-worker-3 - 把文件内容转换成敏感词数组
21:11:03.319 - 14 - ForkJoinPool.commonPool-worker-3 - filterWords = [尼玛, NB, tmd]
21:11:07.319 -  1 - main - main end

3.3 thenRun()

如果我们只想从CompletableFuture的链式操作得到一个完成的通知,甚至都不使用上一步链式操作的结果,那么CompletableFuture.thenRun()是最佳选择,它需要一个Runnable并返回CompletableFuture<Void>

CompletableFuture<Void> thenRun(Runnable action)

需求:我们仅仅想知道 filter_words.txt 的文件是否读取完成

public class ThenRunDemo {
    public static void main(String[] args) {
        // 演示案例:我们仅仅想直到敏感词汇的文件是否读取完成
        CommonUtils.printThreadLog("main start");
        CompletableFuture.supplyAsync(() -> {
            CommonUtils.printThreadLog("开始读取filter_words.txt文件");
            String filterWordContent = CommonUtils.readFile("D:\\JavaWorkSpace\\code\\arithmetic\\algorithm\\src\\com\\zet\\asyn_thread\\filter_words.txt");
            return filterWordContent;
        }).thenRun(() -> {
            CommonUtils.printThreadLog("读取filter_words.txt文件完成");
        });
        CommonUtils.printThreadLog("main continue");
        CommonUtils.sleepSecond(4);
        CommonUtils.printThreadLog("main end");

        /**
         * 总结:
         * thenRun(Runnable runnable):
         * 当异步任务完成后,只想得到一个完成的通知,不使用上一步异步任务的结果,就可以thenRun
         * 通常会把它用在链式操作的末端
         */
    }
}

运行结果

21:12:03.174 -  1 - main - main start
21:12:03.190 -  1 - main - main continue
21:12:03.190 - 14 - ForkJoinPool.commonPool-worker-3 - 开始读取filter_words.txt文件
21:12:03.190 - 14 - ForkJoinPool.commonPool-worker-3 - 读取filter_words.txt文件完成
21:12:07.200 -  1 - main - main end

3.4 进一步提升并行化

CompletableFuture 提供的所有回调方法都有两个异步变体。

CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
// 回调方法的异步变体(异步回调)
CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)

注意:这些带了Async的异步回调 通过在单独的线程中执行回调任务 来帮助进一步促进并行化计算。
需求:异步读取 filter_words.txt 文件中的内容,读取完成后,转换成敏感词数组,主线程获取结果打印输出这个数组

public class ThenApplyAsyncDemo1 {
    public static void main(String[] args) throws Exception {
        CommonUtils.printThreadLog("main start");
        CompletableFuture<String[]> filterWordFuture = CompletableFuture.supplyAsync(() -> {
            CommonUtils.printThreadLog("读取filter_words.txt文件");
            String filterWordsContent = CommonUtils.readFile("D:\\JavaWorkSpace\\code\\arithmetic\\algorithm\\src\\com\\zet\\asyn_thread\\filter_words.txt");
            return filterWordsContent;
        }).thenApply(content -> {
            CommonUtils.printThreadLog("把文件内容转换成敏感词数组");
            String[] filterWords = content.split(",");
            return filterWords;
        });
        CommonUtils.printThreadLog("main continue");
        String[] filterWords = filterWordFuture.get();
        CommonUtils.printThreadLog("filterWords = " + Arrays.toString(filterWords));
        CommonUtils.printThreadLog("main end");
        /**
         * 总结:
         * 一般而言,commonPool为了提高性能
         * thenApply中回调任务和supplyAsync中的异步任务使用的是同一个线程
         * 特殊情况:
         * 如果supplyAsync中的任务是立即返回结果(不是耗时操作), thenApply回调任务会在主线程中执行
         *
         */
    }
}

运行结果

21:13:01.459 -  1 - main - main start
21:13:01.474 -  1 - main - main continue
21:13:01.474 - 14 - ForkJoinPool.commonPool-worker-3 - 读取filter_words.txt文件
21:13:01.474 - 14 - ForkJoinPool.commonPool-worker-3 - 把文件内容转换成敏感词数组
21:13:01.490 -  1 - main - filterWords = [尼玛, NB, tmd]
21:13:01.490 -  1 - main - main end
public class ThenApplyAsyncDemo2 {
    public static void main(String[] args) throws Exception {
        CommonUtils.printThreadLog("main start");
        CompletableFuture<String[]> filterWordFuture = CompletableFuture.supplyAsync(() -> {
            CommonUtils.printThreadLog("读取filter_words.txt文件");
            String filterWordsContent = CommonUtils.readFile("D:\\JavaWorkSpace\\code\\arithmetic\\algorithm\\src\\com\\zet\\asyn_thread\\filter_words.txt");
            return filterWordsContent;
        }).thenApplyAsync(content -> {
            CommonUtils.printThreadLog("把文件内容转换成敏感词数组");
            String[] filterWords = content.split(",");
            return filterWords;
        });
        CommonUtils.printThreadLog("main continue");
        String[] filterWords = filterWordFuture.get();
        CommonUtils.printThreadLog("filterWords = " + Arrays.toString(filterWords));
        CommonUtils.printThreadLog("main end");
    }
}

运行结果

21:13:52.691 -  1 - main - main start
21:13:52.707 - 14 - ForkJoinPool.commonPool-worker-3 - 读取filter_words.txt文件
21:13:52.722 -  1 - main - main continue
21:13:52.722 - 14 - ForkJoinPool.commonPool-worker-3 - 把文件内容转换成敏感词数组
21:13:52.722 -  1 - main - filterWords = [尼玛, NB, tmd]
21:13:52.722 -  1 - main - main end
public class ThenApplyAsyncDemo3 {
    public static void main(String[] args) throws Exception {
        CommonUtils.printThreadLog("main start");
        ExecutorService executor = Executors.newFixedThreadPool(4);
        CompletableFuture<String[]> filterWordFuture = CompletableFuture.supplyAsync(() -> {
            CommonUtils.printThreadLog("读取filter_words.txt文件");
            String filterWordsContent = CommonUtils.readFile("D:\\JavaWorkSpace\\code\\arithmetic\\algorithm\\src\\com\\zet\\asyn_thread\\filter_words.txt");
            return filterWordsContent;
        }).thenApplyAsync(content -> {
            CommonUtils.printThreadLog("把文件内容转换成敏感词数组");
            String[] filterWords = content.split(",");
            return filterWords;
        }, executor);
        CommonUtils.printThreadLog("main continue");
        String[] filterWords = filterWordFuture.get();
        CommonUtils.printThreadLog("filterWords = " + Arrays.toString(filterWords));
        CommonUtils.printThreadLog("main end");
        // 关闭线程池
        executor.shutdown();
    }
}

运行结果

21:14:38.191 -  1 - main - main start
21:14:38.222 - 14 - ForkJoinPool.commonPool-worker-3 - 读取filter_words.txt文件
21:14:38.222 -  1 - main - main continue
21:14:38.222 - 15 - pool-1-thread-1 - 把文件内容转换成敏感词数组
21:14:38.222 -  1 - main - filterWords = [尼玛, NB, tmd]
21:14:38.222 -  1 - main - main end

4. 异步任务编排

4.1 编排2个具有依赖关系的异步任务 thenCompose()

组合上一步异步任务的结果到下一个新的异步任务中,结果由这个新的异步任务返回,此时,可以使用thenCompose() 方法代替,我们可以把它理解为异步任务的组合

CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn)

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

需求:读取敏感词汇文件,将读取到的内容转换为数组

public class ThenComposeDemo2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CommonUtils.printThreadLog("main start");

        CompletableFuture<String[]> filterWordsFuture = CompletableFuture.supplyAsync(() -> {
            CommonUtils.printThreadLog("读取filter_words.txt文件内容");
            String filterWordsContent = CommonUtils.readFile("D:\\JavaWorkSpace\\code\\arithmetic\\algorithm\\src\\com\\zet\\asyn_thread\\filter_words.txt");
            return filterWordsContent;
        }).thenCompose(content -> CompletableFuture.supplyAsync(() -> {
            CommonUtils.printThreadLog("将读取到的内容转换为敏感词数组");
            String[] filterWords = content.split(",");
            return filterWords;
        }));

        CommonUtils.printThreadLog("main continue");
        String[] filterWords = filterWordsFuture.get();
        System.out.println("filterWords = " + Arrays.toString(filterWords));
        CommonUtils.printThreadLog("main end");
    }
}

运行结果

21:15:38.061 -  1 - main - main start
21:15:38.077 - 14 - ForkJoinPool.commonPool-worker-3 - 读取filter_words.txt文件内容
21:15:38.077 -  1 - main - main continue
21:15:38.077 - 14 - ForkJoinPool.commonPool-worker-3 - 将读取到的内容转换为敏感词数组
filterWords = [尼玛, NB, tmd]
21:15:38.093 -  1 - main - main end

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

CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn)
CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn, Executor executor)

4.2 编排2个非依赖关系的异步任务 thenCombine()

我们已经知道,当其中一个Future依赖于另一个Future,使用thenCompose() 用于组合两个Future,如果两个Future之间没有依赖关系,你希望两个Future独立运行并在两者都完成之后执行回调操作时,则使用thenCombine()

// T是第一个任务的结果,U是第二个任务的结果,V是Function应用转换后的结果
CompletableFuture<V> thenCombine(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn)

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

public class ThenCombineDemo1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 读取新闻稿(news.txt)中的敏感词汇,把敏感词汇替换成*,敏感词存储在filter_words.txt中
        CommonUtils.printThreadLog("main start");

        // step1: 读取news.txt文件内容
        CompletableFuture<String> newsFuture = CompletableFuture.supplyAsync(() -> {
            CommonUtils.printThreadLog("读取新闻稿并解析");
            String newsWords = CommonUtils.readFile("D:\\JavaWorkSpace\\code\\arithmetic\\algorithm\\src\\com\\zet\\asyn_thread\\news.txt");
            return newsWords;
        });

        // step2: 读取filter_words.txt文件内容,并解析成敏感词数组
        CompletableFuture<String[]> filterWordsFuture = CompletableFuture.supplyAsync(() -> {
            CommonUtils.printThreadLog("读取敏感词汇并解析");
            String context = CommonUtils.readFile("D:\\JavaWorkSpace\\code\\arithmetic\\algorithm\\src\\com\\zet\\asyn_thread\\filter_words.txt");
            String[] words = context.split(",");
            return words;
        });

        // step3: 替换操作
        CompletableFuture<String> combineFuture = newsFuture.thenCombine(filterWordsFuture, (newsContent, filterWords) -> {
            CommonUtils.printThreadLog("替换敏感词汇");
            for (String filterWord : filterWords) {
                if (newsContent.indexOf(filterWord) >= 0) {
                    newsContent = newsContent.replace(filterWord, "**");
                }
            }
            return newsContent;
        });

        CommonUtils.printThreadLog("main continue");
        String newsContent = combineFuture.get();
        CommonUtils.printThreadLog("news = " + newsContent);
        CommonUtils.printThreadLog("main end");

        /**
         * 总结:
         * thenCombine 用于合并两个没有依赖关系的异步任务
         */
    }
}

运行结果

21:16:45.425 -  1 - main - main start
21:16:45.447 - 15 - ForkJoinPool.commonPool-worker-5 - 读取敏感词汇并解析
21:16:45.447 - 14 - ForkJoinPool.commonPool-worker-3 - 读取新闻稿并解析
21:16:45.447 -  1 - main - main continue
21:16:45.447 - 15 - ForkJoinPool.commonPool-worker-5 - 替换敏感词汇
21:16:45.447 -  1 - main - news = 异步多线程:CompletableFuture**好用
21:16:45.447 -  1 - main - main end

4.3 合并多个异步任务 allOf / anyOf

使用thenCompose() 和 thenCombine() 可以将两个CompletableFuture组合和合并在一起。
如果要编排任意数量的CompletableFuture怎么办?可以使用以下方法来组合任意数量的CompletableFuture

public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)

CompletableFuture.allOf() 用于以下情形中:有多个需要独立并行运行的Future,并在所有这些Future都完成后执行一些操作。

需求:统计news1.txt,news2.txt、news3.txt文件中包含CompletableFuture关键字的文件的个数

public class AllOfDemo {

    public static CompletableFuture<String> readFileFuture(String fileName) {
        return CompletableFuture.supplyAsync(() -> {
            String content = CommonUtils.readFile(fileName);
            return content;
        });
    }

    public static void main(String[] args) {
        // 需求:统计news1.txt,news2.txt、news3.txt文件中包含CompletableFuture关键字的文件的个数
        // step1: 创建list集合存储文件名
        List<String> fileList = Arrays.asList(
                "D:\\JavaWorkSpace\\code\\arithmetic\\algorithm\\src\\com\\zet\\asyn_thread\\news1.txt",
                "D:\\JavaWorkSpace\\code\\arithmetic\\algorithm\\src\\com\\zet\\asyn_thread\\news2.txt",
                "D:\\JavaWorkSpace\\code\\arithmetic\\algorithm\\src\\com\\zet\\asyn_thread\\news3.txt"
        );
        // step2: 根据文件名调用readFileFuture创建多个CompletableFuture,并存入list集合
        List<CompletableFuture<String>> readFileFutureList = fileList.stream().map(fileName -> readFileFuture(fileName)).collect(Collectors.toList());
        // step3: 把list集合转换成数组待用,以便于传入allOf方法中
        CompletableFuture[] readFileFutureArr = readFileFutureList.toArray(new CompletableFuture[readFileFutureList.size()]);
        // step4: 使用allOf方法合并多个异步任务
        CompletableFuture<Void> allOfFuture = CompletableFuture.allOf(readFileFutureArr);
        // step5: 当多个异步任务都完成后,使用回调操作文件结果,统计符合条件的文件个数
        CompletableFuture<Long> countFuture = allOfFuture.thenApply(v -> readFileFutureList.stream().map(future -> future.join()).filter(content -> content.contains("CompletableFuture")).count());
        // step6: 主线程打印出这个文件个数
        Long count = countFuture.join();
        System.out.println("count = " + count);
        /**
         * allOf 特别适合合并多个异步任务,当所有的异步任务都完成时进行操作
         *
         */
    }
}

运行结果

count = 3

当给定的多个异步任务中有任意Future一个完成时,需要执行一些操作,可以使用anyOf 方法

演示案例:anyOf 执行过程

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

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

public class AnyOfDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
            CommonUtils.sleepSecond(2);
            return "Future1的结果";
        });
        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            CommonUtils.sleepSecond(1);
            return "Future2的结果";
        });
        CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> {
            CommonUtils.sleepSecond(3);
            return "Future3的结果";
        });

        CompletableFuture<Object> anyOfFuture = CompletableFuture.anyOf(future1, future2, future3);
        Object object = anyOfFuture.get();
        System.out.println("object = " + object);
    }
}

运行结果

object = Future2的结果

5. 异步任务的异常处理

CompletableFuture 提供了优化处理异常的方式

5.2 exceptionally()

exceptionally用于处理回调链上的异常,回调链上出现的任何异常,回调链不继续向下执行,都在exceptionally中处理异常。

public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)

需求:对回调链中的异常进行处理

public class ExceptionallyDemo {
    public static void main(String[] args) {
        // 异常如何在回调链中传播
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
//            int r = 1 / 0;
            return "result1";
        }).thenApply(result -> {
            CommonUtils.printThreadLog(result);
            String str = null;
            int length = str.length();
            return result + "result2";
        }).thenApply(result -> result + "result3").exceptionally(ex -> {
            System.out.println("出现异常:" + ex.getMessage());
            return "UnKnown";
        });
        /**
         * exceptionally 用于处理回调链上的异常,回调链上出现的任何异常,回调链不继续向下执行,都在exceptionally中处理异常
         */
    }
}

运行结果

21:18:43.456 -  1 - main - result1
出现异常:java.lang.NullPointerException

因为exceptionally只处理一次异常,所以常常用在回调链的末端。

5.2 handle()

CompletableFuture API 还提供了一种更通用的方法 handle() 表示从异常中恢复
handle() 常常被用来恢复回调链中的一次特定的异常,回调链恢复后可以进一步向下传递。

public <U> CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn)

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

public class HandleDemo2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 需求:对回调链中的一次异常进行恢复处理
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            int r = 1 / 0;
            return "result1";
        }).handle((result, ex) -> {
            if (ex != null) {
                CommonUtils.printThreadLog("出现异常:" + ex.getMessage());
                return "UnKnown1";
            }
            return result;
        }).thenApply(result -> {
            String string = null;
            int length = string.length();
            return result + " result2";
        }).handle((result, ex) -> {
            if (ex != null) {
                CommonUtils.printThreadLog("出现异常:" + ex.getMessage());
                return "UnKnown2";
            }
            return result;
        }).thenApply(result -> result + " result3");

        String result = future.get();
        CommonUtils.printThreadLog("result = " + result);
    }
}

运行结果

21:19:36.969 - 14 - ForkJoinPool.commonPool-worker-3 - 出现异常:java.lang.ArithmeticException: / by zero
21:19:36.985 - 14 - ForkJoinPool.commonPool-worker-3 - 出现异常:java.lang.NullPointerException
21:19:37.001 -  1 - main - result = UnKnown2 result3
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值