CompletableFuture 异步编排学习一

前言

CompletableFuture 是 jdk 8 的新特性,CompletableFuture 实现了 CompletionStage 接口和 Future 接口,前者是对后者的一个扩展,增加了异步会点、流式处理、多个 Future 组合处理的能力,使 Java 在处理多任务的协同工作时更加顺畅便利,可用于线程异步编排,使原本串行执行的代码,变为并行执行,提高代码执行速度。

一、CompletableFuture入门

1.1 学习内容

  • 为什么会选择 CompletableFuture
  • 创建 CompletableFuture 异步任务
  • CompletableFuture 异步任务回调
  • CompletableFuture 异步任务编排
  • CompletableFuture 的异常处理

1.2 学习目标

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

1.3 前置知识

  • 熟悉多线程理论知识
  • 接触过 Future 和线程池的经历
  • 会使用 Lambda 表达式和 Stream-API

1.4 Future vs CompletableFuture

1.4.1 准备工作,为了便于后续更好地调试和学习,我们需要定义一个工具类辅助我们对知识的理解。

public class CommonUtils {

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

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

    /**
     * 休眠指定的秒数
     * @param seconds
     */
    public static void sleepSecond(long seconds) {
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

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

}

1.4.2 测试

public class CommonUtilsDemo {

    public static void main(String[] args) {
        // 测试 CommonUtils 工具类
        String content = CommonUtils.readFile("news.txt");
        CommonUtils.printThreadLog(content);
    }
}

控制台打印结果:

1681019966215 |  1 | main | oh my god!completablefuture真tmd好用呀

Process finished with exit code 0

1.4.3 Future 的局限性

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

在这里插入图片描述

public class FutureDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建线程,执行任务
        ExecutorService executor = Executors.newFixedThreadPool(5);
        // step 1: 读取敏感词汇 => thread1
        Future<String[]> filterWordFuture = executor.submit(() -> {
            String str = CommonUtils.readFile("filter_words.txt");
            String[] filterWords = str.split(",");
            return filterWords;
        });

        // step 2: 读取新闻稿 => thread2
        Future<String> newsFuture = executor.submit(() -> {
            return CommonUtils.readFile("news.txt");
        });

        // step 3: 替换操作 => thread3
        Future<String> replaceFuture = executor.submit(() -> {
            String[] words = filterWordFuture.get();
            String news = newsFuture.get();

            for (String word : words) {
                if (news.indexOf(word) >= 0) {
                    news = news.replace(word, "**");
                }
            }
            return news;
        });

        // step 4: 打印输出替换后的新闻稿 => main
        String filteredNews = replaceFuture.get();
        System.out.println("filteredNews = " + filteredNews);
    }
}

运行结果:

filteredNews = oh my god!completablefuture真**好用呀

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

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

1.4.4 CompletableFuture 优势
CompletableFuture 实现了 FutureCompletionStage 接口

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

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

二、创建异步任务

2.1 runAsync

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

static CompletableFuture<Void> runAsync(Runnable runnable);

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

public class RunAsyncDemo {
    public static void main(String[] args) {
        // runAsync 创建异步任务
        CommonUtils.printThreadLog("main start");
        // 使用Runnable匿名内部类
        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); //  此处休眠为的是等待CompletableFuture背后的线程池执行完成。
        CommonUtils.printThreadLog("main end");
    }
}

运行结果:

1681023703382 |  1 | main | main start
1681023703430 |  1 | main | here are not blocked,main continue
1681023703430 | 15 | ForkJoinPool.commonPool-worker-3 | 读取文件开始
1681023706445 | 15 | ForkJoinPool.commonPool-worker-3 | 读取文件结束
1681023707443 |  1 | main | main end

Process finished with exit code 0

我们也可以以 Lambda 表达式的形式传递 Runnable 接口实现类对象

public class RunAsyncDemo2 {
    public static void main(String[] args) {
        // runAsync 创建异步任务
        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);
        CommonUtils.printThreadLog("main end");
    }
}

运行结果:

1681026452040 |  1 | main | main start
1681026452059 |  1 | main | here are not blocked,main continue
1681026452059 | 15 | ForkJoinPool.commonPool-worker-3 | 读取文件开始
1681026455065 | 15 | ForkJoinPool.commonPool-worker-3 | 读取文件结束
1681026456068 |  1 | main | main end

Process finished with exit code 0

需求:使用 CompletableFuture 开启异步任务读取 news.txt 文件中的新闻稿,并打印输出。

public class RunAsyncDemo3 {
    public static void main(String[] args) {
        // 需求:使用CompletableFuture开启异步任务读取 news.txt 文件中的新闻稿,并打印输出。
        CommonUtils.printThreadLog("main start");

        CompletableFuture.runAsync(()->{
            String news = CommonUtils.readFile("news.txt");
            CommonUtils.printThreadLog(news);
        });

        CommonUtils.printThreadLog("here are not blocked,main continue");
        CommonUtils.sleepSecond(4);
        CommonUtils.printThreadLog("main end");
    }
}

运行结果:

1681026694093 |  1 | main | main start
1681026694114 |  1 | main | here not blocked main continue
1681026694116 | 15 | ForkJoinPool.commonPool-worker-3 | 读取文件
oh my god!completablefuture真tmd好用呀
1681026698122 |  1 | main | main end

Process finished with exit code 0

在后续的章节中,我们会经常使用 Lambda 表达式。

2.2 supplyAsync

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

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

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

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

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

        CompletableFuture<String> newsFuture = CompletableFuture.supplyAsync(new Supplier<String>() {
            @Override
            public String get() {
                String news = CommonUtils.readFile("news.txt");
                return news;
            }
        });

        CommonUtils.printThreadLog("here are not blocked,main continue");
        // 阻塞并等待newsFuture完成
        String news = newsFuture.get();
        CommonUtils.printThreadLog("news = " + news);
        CommonUtils.printThreadLog("main end");
    }
}

运行结果:

1681027765405 |  1 | main | main start
1681027765436 |  1 | main | here not blocked main continue
news = oh my god!completablefuture真tmd好用呀
1681027765442 |  1 | main | main end

Process finished with exit code 0

如果想要获取 newsFuture 结果,可以调用 completableFuture.get() 方法,get() 方法将阻塞,直到 newsFuture 完成。我们依然可以使用Java 8的 Lambda 表达式使上面的代码更简洁。

CompletableFuture<String> newsFuture = CompletableFuture.supplyAsync(() -> {
    String news = CommonUtils.readFile("news.txt");
    return news;
});

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 {
        CommonUtils.printThreadLog("main start");

        ExecutorService executor = Executors.newFixedThreadPool(4);
        // 使用lambda表达式
        CompletableFuture<String> newsFuture = CompletableFuture.supplyAsync(() -> {
            CommonUtils.printThreadLog("异步读取文件开始");
            String content = CommonUtils.readFile("news.txt");
            return content;
        },executor);

        CommonUtils.printThreadLog("here not blocked main continue");
        String news = newsFuture.get();
        System.out.println("news = " + news);
        // 关闭线程池
        executor.shutdown();
        CommonUtils.printThreadLog("main end");
    }
}

运行结果:

1681028776822 |  1 | main | main start
1681028776843 |  1 | main | here not blocked main continue
1681028776843 | 15 | pool-1-thread-1 | 异步读取文件开始
news = oh my god!completablefuture真tmd好用呀
1681028776848 |  1 | main | main end

Process finished with exit code 0

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

2.4 异步编程思想

​综合上述,看到了吧,我们没有显式地创建线程,更没有涉及线程通信的概念,整个过程根本就没涉及线程知识吧,以上专业的说法是:线程的创建和线程负责的任务进行解耦,它给我们带来的好处线程的创建和启动全部交给线程池负责,具体任务的编写就交给程序员,专人专事

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

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

三、异步任务回调

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<R> thenApply(Function<T,R> fn)

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

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

        CompletableFuture<String> readFileFuture = CompletableFuture.supplyAsync(() -> {
            CommonUtils.printThreadLog("读取filter_words文件");
            String filterWordsContent = CommonUtils.readFile("filter_words.txt");
            return filterWordsContent;
        });

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

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

运行结果:

1681041282908 |  1 | main | main start
1681041282930 | 15 | ForkJoinPool.commonPool-worker-3 | 读取filter_words文件
1681041282930 |  1 | main | main continue
1681041282932 | 15 | ForkJoinPool.commonPool-worker-3 | 文件内容转换成敏感词数组
1681041282934 |  1 | main | filterWords = [尼玛, NB, tmd]
1681041282935 |  1 | main | main end

Process finished with exit code 0

你还可以通过附加一系列thenApply()回调方法,在CompletableFuture上编写一系列转换序列。一个thenApply()方法的结果可以传递给序列中的下一个,如果你对链式操作很了解,你会发现结果可以在链式操作上传递。

CompletableFuture<String[]> filterWordsFuture = CompletableFuture.supplyAsync(() -> {
    CommonUtils.printThreadLog("读取filter_words文件");
    String filterWordsContent = CommonUtils.readFile("filter_words.txt");
    return filterWordsContent;
}).thenApply((content) -> {
    CommonUtils.printThreadLog("转换成敏感词数组");
    String[] filterWords = content.split(",");
    return filterWords;
});

3.2 thenAccept

如果你不想从回调函数返回结果,而只想在 Future 完成后运行一些代码,则可以使用thenAccept()

这些方法是入参一个 Consumer,它可以对异步任务的执行结果进行消费使用,方法返回 CCompletableFuture<Void>

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

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

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

public class ThenAcceptDemo {
    public static void main(String[] args) {
        CommonUtils.printThreadLog("main start");

        CompletableFuture.supplyAsync(() -> {
            CommonUtils.printThreadLog("读取filter_words文件");
            String filterWordsContent = CommonUtils.readFile("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");
    }
}

3.3 thenRun

前面我们已经知道,通过thenApply( Function<T,R> ) 对链式操作中的上一个异步任务的结果进行转换,返回一个新的结果;通过thenAccept( Consumer ) 对链式操作中上一个异步任务的结果进行消费使用,不返回新结果;如果我们只是想从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文件");
            String filterWordsContent = CommonUtils.readFile("filter_words.txt");
            return filterWordsContent;
        }).thenRun(() -> {
            CommonUtils.printThreadLog("读取filter_words文件读取完成");
        });

        CommonUtils.printThreadLog("main continue");
        CommonUtils.sleepSecond(4);
        CommonUtils.printThreadLog("main end");
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值