并发编程神器CompletableFuture

1,070 阅读12分钟

一、Future vs CompletableFuture

1、Future的局限性

public static void main(String[] args) throws ExecutionException, InterruptedException {
    ExecutorService ex = Executors.newFixedThreadPool(5);
    //读取敏感词汇
    Future<String[]> filterWords = ex.submit(() -> {
        String str = CommonUtils.readFile("filter_words.txt");
        return str.split(",");
    });
    //读取新闻稿
    Future<String> news = ex.submit(() -> CommonUtils.readFile("news.txt"));
    //替换操作
    Future<String> replaceStr = ex.submit(() -> {
        String[] words = filterWords.get();
        String str = news.get();
        for (String word : words) {
            if (str.contains(word)) {
                str = str.replace(word, "**");
            }
        }
        return str;
    });
    //打印输出替换后的新闻稿
    String s = replaceStr.get();
    System.out.println(s);
}

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

2、CompletableFuture的优势

image.png转存失败,建议直接上传图片文件 CompletableFuture相对于Future具有以下的优势:

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

二、创建异步任务

1、runAsync

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

static CompletableFuture<Void> runAsync(Runnable runnable)
public static void main(String[] args) {
    // runAsync 创建异步任务
    CommonUtils.printTheadLog("main start");
    // 使用Runnable匿名内部类
    CompletableFuture.runAsync(new Runnable() {
        @Override
        public void run() {
            CommonUtils.printTheadLog("读取文件开始");
            // 使用睡眠来模拟一个长时间的工作任务(例如读取文件,网络请求等)
            CommonUtils.sleepSecond(3);
            CommonUtils.printTheadLog("读取文件结束");
        }
    });
    CommonUtils.printTheadLog("here are not blacked,main continue");
    CommonUtils.sleepSecond(4); // 此处休眠 为的是等待CompletableFuture背后的线程池执行完成。
    CommonUtils.printTheadLog("main end");
}

异步任务的创建并不会立刻执行,而是会在抢到PCPU时间片后才会执行,异步任务的本质就是以开启线程的方式去执行任务!!!

2、supplyAsync

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

static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static void main(String[] args) throws ExecutionException, InterruptedException {
    //需 求:开启异步任务读取news.txt文件中的新闻稿,返回文件中内容并在主线程打印输出

    CommonUtils.printTheadLog("main start");

    CompletableFuture<String> future = CompletableFuture.supplyAsync(new Supplier<String>() {

        @Override
        public String get() {
            String news = CommonUtils.readFile("E:\\project\\competable-future\\src\\main\\java\\news.txt");
            return news;
        }
    });

    CommonUtils.printTheadLog("here are not blocked,main continue");
    //阻塞并等待future完成
    String news = future.get();

    CommonUtils.printTheadLog("news ="+news);
    CommonUtils.printTheadLog("main end");
}

如果想要获取上述代码中future中的结果,可以调用completableFuture.get()方法,get()将阻塞,直到future完成。

3、异步任务中的线程池

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)
// spplyAsync()重载
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,Executor executor)
public static void main(String[] args) throws ExecutionException, InterruptedException {
    ExecutorService pool = Executors.newFixedThreadPool(4);
    CommonUtils.printTheadLog("main start");
    CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
        CommonUtils.printTheadLog("开始异步执行任务");
        return CommonUtils.readFile("news.txt");
    }, pool);
    String news = future.get();
    System.out.println("news:" + news);
    CommonUtils.printTheadLog("here are not blacked,main continue");
    CommonUtils.printTheadLog("main end");
    pool.shutdown();
}

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

4、异步编程思想

综合上述,我们并没有显示地创建线程,更没有涉及线程通信的概念,整个过程根本就没涉及线程知识,以上专业的说法是:线程的创建和线程负责的任务进行解耦,它给我们带来的好处线程的创建和启动全部交给线程池负责,具体任务的编写交给程序员,专人专事。 异步编程是可以让程序并行(也可能是并发)运行的一种手段,其可以让程序中的一个工作单元作为异步任务与主线程分开独立运行,并且在异步任务运行结束后,会通知主线程它的运行结果或者失败原因,毫无疑问,一个异步任务其实就是开启一个线程来完成的,使用异步编程可以提高应用程序的性能和响应能力等。 作为开发者,只需要有一个意识: 开发者只需要把耗时的操作交给CompletableFuture开一个异步任务,然后继续关注主线程业务,当异步任务运行完成时会通知主线程它的运行结果。我们把具备了这种编程思想的开发称为异步编程思想。

三、异步任务的回调

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

1、thenApply

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

CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
public static void main(String[] args) throws ExecutionException, InterruptedException {
    CommonUtils.printTheadLog("main start");
    CompletableFuture<String[]> thenApply = CompletableFuture.supplyAsync(() ->
                                                                          CommonUtils.readFile("filter_words.txt"))
    .thenApply(s -> s.split(","));
    String[] strings = thenApply.get();
    for (String s : strings) {
        CommonUtils.printTheadLog(s);
    }
    CommonUtils.printTheadLog("here are not blacked,main continue");
    CommonUtils.printTheadLog("main end");
}

2、thenAccept

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

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

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

public static void main(String[] args) throws ExecutionException, InterruptedException {
    CommonUtils.printTheadLog("main start");
    CompletableFuture.supplyAsync(() ->
                                  CommonUtils.readFile("filter_words.txt"))
    .thenApply(s -> s.split(","))
    .thenAccept(strings -> {
        for (String string : strings) {
            CommonUtils.printTheadLog(string);
        }
    });
    CommonUtils.sleepSecond(4);
    CommonUtils.printTheadLog("here are not blacked,main continue");
    CommonUtils.printTheadLog("main end");
}

3、thenRun

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

CompletableFuture<Void> thenRun(Runnable action);
public static void main(String[] args) throws ExecutionException, InterruptedException {
    CommonUtils.printTheadLog("main start");
    CompletableFuture.supplyAsync(() -> {
        CommonUtils.printTheadLog("正在读取敏感词汇。。。");
        return CommonUtils.readFile("filter_words.txt");
    })
    .thenRun(() -> CommonUtils.printTheadLog("敏感词汇读取完成"));
    CommonUtils.sleepSecond(4);
    CommonUtils.printTheadLog("here are not blacked,main continue");
    CommonUtils.printTheadLog("main end");
}

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)
// thenAccept和其异步回调
CompletableFuture<Void> thenAccept(Consumer<? super T> action)
CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action)
CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action Executor executor)
// thenRun和其异步回调
CompletableFuture<Void> thenRun(Runnable action)
CompletableFuture<Void> thenRunAsync(Runnable action)
CompletableFuture<Void> thenRunAsync(Runnable action,Executor executor)

这些带了Async的异步回调通过在单独的线程中执行回调任务来帮我们去进一步促进并行计算。

  • 一般而言,commonPool为了提高性能,并不会立马收回线程,thenApply中回调任务和supplyAsync中的异步任务使用的是同一个线程。
  • 这里存在一个特殊情况,即如果supplyAsync中的任务是立即返回结果(不是耗时的任务),那么thenApply回调任务也会在主线程执。

要更好地控制执行回调任务的线程,可以使用异步回调。如果使用thenApplyAsync()回调,那么它将在从ForkJoinPool.commonPool()获取另一个线程执行(概率获取),一般情况是接着使用上一次的线程。CompletableFuture内部优化的结果。 另外,如果将Executor传递给thenApplyAsync()回调,则该回调的异步任务将在从Excutor的线程池中获取的线程中执行另外,如果将Executor传递给thenApplyAsync()回调,则该回调的异步任务将在从Excutor的线程池中获取的线程中执行。 image.png

四、异步任务编排

1、编排2个依赖关系的异步任务thenCompose

需求:异步读取filter_words.txt文件中的内容,读取完成后,转换成敏感词数组让主线程待用。

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

public static CompletableFuture<String[]> splitFuture(String content) {
return CompletableFuture.supplyAsync(()->{
    String[] filterWords = content.split(",");
    return filterWords;
});
}


public static void main(String[] args) throws ExecutionException, InterruptedException {
    //编排2个依赖关系的异步任务thenCompose()
    //使用thenApply
    CompletableFuture<CompletableFuture<String[]>> future = readFileFuture("E:\\project\\competable-future\\src\\main\\java\\filter_words.txt")
    .thenApply(content -> {
        return splitFuture(content);
    });

    CompletableFuture<String[]> completableFuture = future.get();

    System.out.println("completableFuture = " + completableFuture);

    String[] content = completableFuture.get();

    System.out.println("content = " + content);
}

在上面的案例中,thenApply(Function<T,R>)中Function回调会对上一步异步结果转换后得到一个简单值,但现在这种情况下,最终结果是嵌套的CompletableFuture,所以这是不符合预期的,那怎么办呢? 我们想要的:把上一步异步任务的结果,转成一个CompletableFuture对象,这个Completable对象中包含本次异步任务处理后的结果。也就是说,我们想结合上一步异步任务的结果得到下一个新的异步任务中,结果由这个新的异步任务返回。 此时,你需要使用 thenCompose() 方法代替, 我们可以把它理解为异步任务的组合:

CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn)
ublic class ThenComposeTest {
    public static CompletableFuture<String> readFileFuture(String fileName) {
        return CompletableFuture.supplyAsync(() -> {
            String filterWordsContent = CommonUtils.readFile(fileName);
            return filterWordsContent;
        });
    }

    public static CompletableFuture<String[]> splitFuture(String content) {
        return CompletableFuture.supplyAsync(() -> {
            String[] filterWords = content.split(",");
            return filterWords;
        });
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String[]> completableFuture = readFileFuture("filter_words.txt")
        .thenCompose(s -> splitFuture(s));
        String[] strings = completableFuture.get();
        CommonUtils.printTheadLog(Arrays.toString(strings));
    }
}

代码优化:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    CompletableFuture<String[]> filterWords = CompletableFuture
    .supplyAsync(() ->
                 CommonUtils.readFile("filter_words.txt"))
    .thenCompose(s ->
                 CompletableFuture.supplyAsync(() -> s.split(",")));
    CommonUtils.printTheadLog(Arrays.toString(filterWords.get()));
}

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

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

2、编排2个非依赖关系的异步任务thenCombine

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

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

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

public static void main(String[] args) throws ExecutionException, InterruptedException {
    CommonUtils.printTheadLog("main start");
    //读取敏感词汇
    CompletableFuture<String[]> future1 = CompletableFuture.supplyAsync(() -> {
        CommonUtils.printTheadLog("开始读取filter_words.txt");
        String str = CommonUtils.readFile("filter_words.txt");
        CommonUtils.printTheadLog("读取filter_words.txt结束");
        return str.split(",");
    });
    //读取新闻稿
    CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
        CommonUtils.printTheadLog("开始读取news.txt");
        String str = CommonUtils.readFile("news.txt");
        CommonUtils.printTheadLog("读取news.txt结束");
        return str;
    });
    //将新闻稿敏感词汇替换成**
    CompletableFuture<String> future3 = future1.thenCombine(future2, (filterWord, news) -> {
        CommonUtils.printTheadLog("开始替换敏感词汇");
        for (String word : filterWord) {
            if (news.contains(word)) {
                news = news.replace(word, "**");
            }
        }
        CommonUtils.printTheadLog("替换敏感词汇结束");
        return news;
    });
    CommonUtils.printTheadLog("main continue");
    String content = future3.get();
    CommonUtils.printTheadLog("content="+content);
    CommonUtils.printTheadLog("main stop");
}

当两个Future都完成时,才将两个异步任务的结果传递给thenCombine的回调函数进一步处理!!! thenCombine 也存在异步回调变体版本:

CompletableFuture<V> thenCombine(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn)
CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn)
CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn, Executor executor)

3、编排多个异步任务allOf

适用场景:有多个需要独立并运行的Future,并在所有这些Future都完成后执行一些操作

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

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

public class AllOfTest {
    public static CompletableFuture<String> readNews(String pathName) {
        return CompletableFuture.supplyAsync(() -> CommonUtils.readFile(pathName));
    }

    public static void main(String[] args) {
        List<String> fileNames = Arrays.asList("news1.txt", "news2.txt", "news3.txt");
        List<CompletableFuture<String>> completableFutures = fileNames
        .stream()
        .map(AllOfTest::readNews)
        .collect(Collectors.toList());
        int len = completableFutures.size();
        CompletableFuture[] completableFuturesArray = completableFutures.toArray(new CompletableFuture[len]);
        CompletableFuture<Long> completablefuture = CompletableFuture.allOf(completableFuturesArray)
        .thenApply(unused -> completableFutures.stream()
                   .map(stringCompletableFuture -> stringCompletableFuture.join())
                   .filter(s -> s.contains("completablefuture"))
                   .count());
        Long count = completablefuture.join();
        CommonUtils.printTheadLog("count =" + count);
    }
}

allOf 特别适合合并多个异步任务,当所有异步任务都完成时可以进一步操作。

4、编排多个异步任务anyOf

适用场景:当给定的多个异步任务中的有任意Future一个完成时,需要执行一些操作,就可以使用它

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

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

public static void main(String[] args) throws ExecutionException, InterruptedException {

    //anyOf()
    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 ret = anyOfFuture.get();
    System.out.println("ret = " + ret);
}

在上面的示例中,当一个CompletableFuture中的任意一个完成时,anyOfFuture就完成了。由于future2的睡眠时间最少,因此它将首先完成,最终结果将是"Future2的结果"。

  • anyOf() 方法返回类型必须是 CompletableFutue 。
  • anyOf()的问题在于,如果你拥有返回不同类型结果的CompletableFuture,那么你将不知道最终CompletableFuture的类型。