Java并发工具类CompletableFuture教程示例

Java 8带来了大量的新功能和增强功能,例如Lambda表达式,Streams,CompletableFutures等。在本文中,我将通过简单的示例向您详细说明CompletableFuture及其所有方法。

1.What's a CompletableFuture?

首先了解什么是CompletableFuture,它是用于Java中的异步编程。异步编程是一种编写非阻塞代码的方法,它通过在主应用程序线程独立的线程上运行任务,并通知主线程其进度、完成或失败。 这样,你的主线程就不会阻塞、等待任务的完成,它可以并行地执行其他任务。 拥有这种并行性可以极大地提高程序的性能。

2.Future vs CompletableFuture

CompletableFuture是Java Future API的扩展,该API是在Java 5中引入的。 Future用作异步计算结果的引用。它提供了一个isDone()方法来检查计算是否完成,以及一个get()方法来检索计算完成时的结果。

Future的API向Java异步编程迈出了很好的一步,但是它缺少一些重要和有用的特性;

2.1.Future的局限性

1.无法手动完成

假设您已经编写了一个函数来从远程API获取电子商务产品的最新价格。由于这个API调用非常耗时,所以您在单独的线程中运行它,并从函数中返回一个Future。 现在,假设如果远程API服务宕机,那么您希望通过产品的最后缓存价格手动完成将来的工作。 你能用Future做到这一点吗?肯定是不能的;

2.在没有阻塞的情况下,您无法对Future的结果执行进一步的操作

Future不会通知你它的完成。它提供了一个get()方法,该方法阻塞直到结果可用为止。 您不具备将回调函数附加到Future并在Future的结果可用时自动调用它的能力。

3.无法解决任务相互依赖的问题

有时您需要执行一个长时间运行的计算,当计算完成时,您需要将其结果发送到另一个长时间运行的计算,依此类推。 您无法使用Future创建此类异步工作流。

4.不能将多个Future合并在一起

假设你有10种不同的Future,你想并行运行在它们全部完成后然后运行某个函数,你不能这样使用Future

5.没有异常处理

Future的API没有任何异常处理机制。

看吧!有很多限制,对吧?这就是为什么我们有CompletableFuture。你可以通过CompletableFuture实现以上所有需求。 CompletableFuture继承了FutureCompletionStage接口,并为创建、链接依赖和组合多个Future提供了大量的便利方法。它还提供了非常全面的异常处理支持。

3.Creating a CompletableFuture

3.1 一个最简单的例子

您只需使用CompletableFuture的无参构造函数即可创建

CompletableFuture<String> completableFuture = new CompletableFuture<String>();
复制代码

这是您可以拥有创建CompletableFutured的最简单的方法。所有想要获取此completableFuture结果的客户端都可以调用completableFuture.get()方法;

String result = completableFuture.get();
复制代码

get()方法将阻塞,直到Future完成。因此,上述调用将永远被阻止,因为Future从未完成。 您可以使用CompletableFuture.complete()方法手动完成Future

CompletableFuture.complete("Future的结果");
复制代码

所有等待这个Future的客户端都将得到指定的结果。并且对completableFuture.complete()的后续调用将被忽略。

3.2 使用runAsync()运行一个没有返回值的异步任务

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

//异步运行Runnable对象的任务。
CompletableFuture<Void> future = CompletableFuture.runAsync(new Runnable() {
    @Override
    public void run() {
        //这里用睡眠来模拟一个长时间的工作任务
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new IllegalStateException(e);
        }
        System.out.println("我将在与主线程不同的单独线程中运行。");
    }
});
//阻塞并等待Future完成
future.get()
复制代码

您还可以以Lambda表达式的形式传递Runnable对象

//使用Lambda表达式
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    // 这里用睡眠来模拟一个长时间的工作任务
    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        throw new IllegalStateException(e);
    }
    System.out.println("我将在与主线程不同的单独线程中运行。");
});
复制代码

在这篇文章中,我会经常使用Lambda表达式,如果您尚未在Java代码中使用过Lambda表达式,则也应该使用它,在未来它是一种编码趋势;

3.3 使用supplyAsync()运行一个有返回值的异步任务

CompletableFuture.runAsync()对于不返回任何内容的任务很有用。但是,如果您想从后台任务返回一些结果怎么办? 好吧,CompletableFuture.supplyAsync()是您的伴侣。它接受Supplier<T>并返回CompletableFuture<T>,其中T是通过调用给定供提供者获得的值的类型

//异步运行Supplier对象指定的任务
CompletableFuture<String> future = CompletableFuture.supplyAsync(new Supplier<String>() {
    @Override
    public String get() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new IllegalStateException(e);
        }
        return "异步计算的结果";
    }
});
//阻塞并等待Future完成
String result = future.get();
System.out.println(result);
复制代码

Supplier<T>是一个简单的函数接口,代表结果的提供者。 它只有一个get()方法,您可以在其中编写后台任务并返回结果。 再一次,您可以使用Java 8的Lambda表达式使上面的代码更简洁

//使用Lambda表达式
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        throw new IllegalStateException(e);
    }
    return "异步计算的结果";
});
复制代码

关于Executor和Thread Pool的说明

大家可能知道,runAsync()和supplyAsync()方法在单独的线程中执行其任务。 但是,我们从未创建线程对吗? 是的! CompletableFuture会从全局ForkJoinPool.commonPool()获取的线程中执行这些任务;

但是,您也可以创建一个线程池,并将其传递给runAsync()和supplyAsync()方法,以使它们在从您的线程池获得的线程中执行任务。 CompletableFuture API中的所有方法都有两种变体,一种是接受Executor作为参数,而另一种则接收;

// Variations of runAsync() and supplyAsync() methods
static CompletableFuture<Void>  runAsync(Runnable runnable)
static CompletableFuture<Void>  runAsync(Runnable runnable, Executor executor)
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
复制代码

您可以按照以下方法创建线程池并将其传递给以下方法

Executor executor = Executors.newFixedThreadPool(10);
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        throw new IllegalStateException(e);
    }
    return "异步计算的结果";
}, executor);
复制代码

4.基于CompletableFuture上转换和执行

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

在此之前,当然也有其他方法可以实现,相信大家应该了解过Guava中提供了一个ListenableFuture, 他是扩展了Future接口,然后提供了回调监听实现,有兴趣的可以了解;

方法一:通过ListenableFuture的addListener方法

listenableFuture.addListener(new Runnable() {
    @Override
    public void run() {
        try {
            System.out.println("获取Listenable future的结果:" + listenableFuture.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}, executorService);
复制代码

方法二:通过Futures的静态方法addCallback给ListenableFuture添加回调函数

Futures.addCallback(listenableFuture, new FutureCallback<Integer>() {
    @Override
    public void onSuccess(Integer result) {
        System.out.println("获取带有回调接口的listenableFuture " + result);
    }

    @Override
    public void onFailure(Throwable t) {
        t.printStackTrace();
    }
});
复制代码

4.1.thenApply()

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

//创建一个CompletableFuture
CompletableFuture<String> whatsYourNameFuture = CompletableFuture.supplyAsync(() -> {
   try {
       TimeUnit.SECONDS.sleep(2);
   } catch (InterruptedException e) {
       throw new IllegalStateException(e);
   }
   return "Jack";
});

//使用thenApply()将回调附加到Future
CompletableFuture<String> greetingFuture = whatsYourNameFuture.thenApply(name -> {
   return "Hello " + name;
});

//阻塞并获取Future的结果
System.out.println(greetingFuture.get()); // Hello Jack
复制代码

您还可以通过附加一系列thenApply()回调方法,在CompletableFuture上编写一系列转换序列。一个thenApply()方法的结果传递给系列中的下一个:

CompletableFuture<String> welcomeText = CompletableFuture.supplyAsync(() -> {
    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
       throw new IllegalStateException(e);
    }
    return "Jack";
}).thenApply(name -> {
    return "Hello " + name;
}).thenApply(greeting -> {
    return greeting + ", 欢迎来到Ratel的博客";
});

System.out.println(welcomeText.get());
// 打印:Hello Jack, 欢迎来到Ratel的博客
复制代码

4.2. thenAccept() and thenRun()

如果您不想从回调函数返回任何内容,而只想在Future完成后运行一些代码,则可以使用thenAccept() and thenRun()方法。 这些方法是消费者,通常用作回调链中的最后一个回调。 CompletableFuture.thenAccept()接受Consumer<T>并返回CompletableFuture<Void>。 它可以访问附加了它的CompletableFuture的结果。

// thenAccept()例子
CompletableFuture.supplyAsync(() -> {
	return ProductService.getProductDetail(productId);
}).thenAccept(product -> {
	System.out.println("从远程服务获取商品名称: " + product.getName())
});
复制代码

CompletableFuture.thenAccept()可以访问其所连接的CompletableFuture的结果,而CompletableFuture.thenRun()甚至都不能访问Future的结果。它需要一个Runnable并返回CompletableFuture<Void>

// thenRun() example
CompletableFuture.supplyAsync(() -> {
    //运行一些计算逻辑的代码
}).thenRun(() -> {
    //计算完成要做的事情
});
复制代码

关于异步访问的说明

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

// thenApply() variants
<U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
<U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
<U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)
//其实还有方法都有异步两种变体,大家下来可以自己看。
复制代码

这些异步回调变体通过在单独的线程中执行回调任务来帮助您进一步并行化计算,可参考下面的例子

CompletableFuture.supplyAsync(() -> {
    try {
       TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
      throw new IllegalStateException(e);
    }
    return "Some Result"
}).thenApply(result -> {
    /* 
      在执行supplyAsync()任务的同一线程中执行 或在主线程中,如果supplyAsync()任务立即完成(删除sleep()调用以进行验证)
    */
    return "Processed Result"
})
复制代码

在上述情况下,thenApply()内的任务在执行supplyAsync()任务的同一线程中执行,或者如果supplyAsync()任务立即完成,则在主线程中执行(尝试删除sleep()调用以进行验证).

要更好地控制执行回调任务的线程,可以使用异步回调。如果使用thenApplyAsync()回调,那么它将在从ForkJoinPool.commonPool()获得的另一个线程中执行.

CompletableFuture.supplyAsync(() -> {
    return "Some Result"
}).thenApplyAsync(result -> {
    // Executed in a different thread from ForkJoinPool.commonPool()
    return "Processed Result"
})
复制代码

此外,如果将Executor传递给thenApplyAsync()回调,则该任务将在从Executor的线程池中获取的线程中执行;

Executor executor = Executors.newFixedThreadPool(2);
CompletableFuture.supplyAsync(() -> {
    return "Some result"
}).thenApplyAsync(result -> {
    // Executed in a thread obtained from the executor
    return "Processed Result"
}, executor);
复制代码

4.3.将两个CompletableFutures组合在一起

Java并发工具类CompletableFuture教程示例

4.3.1.使用thenCompose()合并两个依赖的Future

假设您要从远程API服务中获取用户的详细信息,并且一旦该用户的详细信息可用,就希望从另一服务中获取该用户的信用等级,考虑以下getUserDetail()和getCreditRating()方法的实现。

CompletableFuture<User> getUsersDetail(String userId) {
	return CompletableFuture.supplyAsync(() -> {
		return UserService.getUserDetails(userId);
	});	
}

CompletableFuture<Double> getCreditRating(User user) {
	return CompletableFuture.supplyAsync(() -> {
		return CreditRatingService.getCreditRating(user);
	});
}
复制代码

现在,让我们了解如果使用thenApply()达到预期的结果会发生什么

CompletableFuture<CompletableFuture<Double>> result = getUserDetail(userId)
.thenApply(user -> getCreditRating(user));
复制代码

在前面的示例中,传递给thenApply()回调的Supplier函数将返回一个简单值,但在这种情况下,它将返回CompletableFuture。因此,上述情况的最终结果是嵌套的CompletableFuture,所以这是不符合预期的,那怎么办呢? 如果希望最终结果是顶层Future,请使用thenCompose()方法代替

CompletableFuture<Double> result = getUserDetail(userId)
.thenCompose(user -> getCreditRating(user));
复制代码

因此,这里有一个经验法则-如果您的回调函数返回CompletableFuture,并且您希望从CompletableFuture链中获得平坦的结果(在大多数情况下,您会希望这样做),则可以使用thenCompose().

4.3.2.使用thenCombine()合并两个独立的Future

当其中一个Future依赖于另一个Future,使用thenCompose()用于合并两个Future时,而当您希望两个Future独立运行并在两者都完成之后执行某项操作时,则使用thenCombine();

System.out.println("获取体重.");
CompletableFuture<Double> weightInKgFuture = CompletableFuture.supplyAsync(() -> {
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
       throw new IllegalStateException(e);
    }
    return 65.0;
});

System.out.println("获取身高.");
CompletableFuture<Double> heightInCmFuture = CompletableFuture.supplyAsync(() -> {
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
       throw new IllegalStateException(e);
    }
    return 177.8;
});

System.out.println("计算体重指数");
CompletableFuture<Double> combinedFuture = weightInKgFuture
        .thenCombine(heightInCmFuture, (weightInKg, heightInCm) -> {
    Double heightInMeter = heightInCm / 100;
    return weightInKg / (heightInMeter * heightInMeter);
});

System.out.println("你的体重指数为:" + combinedFuture.get());
复制代码

当两个Future都完成时,将调用传递给thenCombine()的回调函数.

4.3.3.将多个CompletableFuture组合在一起

我们使用thenCompose()和thenCombine()将两个CompletableFuture组合在一起。现在,如果要组合任意数量的CompletableFuture怎么办?好了,您可以使用以下方法来组合任意数量的CompletableFuture.

static CompletableFuture<Void>	 allOf(CompletableFuture<?>... cfs)
static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)
复制代码

4.3.3.1.CompletableFuture.allOf()

CompletableFuture.allOf()用于以下情形中:您具有要并行运行的独立Future的列表,并在所有这些Future完成后执行一些操作.

假设您要下载网站的100个不同网页的内容。 您可以顺序执行此操作,但这将花费大量时间。 因此,您编写了一个函数,该函数需要一个网页链接,并返回一个CompletableFuture,即它异步下载该网页的内容;

CompletableFuture<String> downloadWebPage(String pageLink) {
	return CompletableFuture.supplyAsync(() -> {
		// 下载并返回网页内容的代码
	});
} 
复制代码

现在,下载所有网页后,您要计算包含关键字“CompletableFuture”的网页数。让我们使用CompletableFuture.allOf()来实现这一目标.

List<String> webPageLinks = Arrays.asList(...)	//100个网页的链接地址列表

//异步下载所有网页的内容
List<CompletableFuture<String>> pageContentFutures = webPageLinks.stream()
        .map(webPageLink -> downloadWebPage(webPageLink))
        .collect(Collectors.toList());


// 使用allOf()方法创建一个合并的Future
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
        pageContentFutures.toArray(new CompletableFuture[pageContentFutures.size()])
);
复制代码

CompletableFuture.allOf()的问题在于它返回CompletableFuture<Void>。但是我们可以通过编写一些额外的代码行来获取所有包装的CompletableFuture的结果.

// 当所有Future都完成后,调用future.join()以获取其结果并将结果收集在列表中
CompletableFuture<List<String>> allPageContentsFuture = allFutures.thenApply(v -> {
   return pageContentFutures.stream()
           .map(pageContentFuture -> pageContentFuture.join())
           .collect(Collectors.toList());
});
复制代码

花一点时间来理解上面的代码片段。由于我们在所有的Future都完成后才调用future.join(),因此我们不会在任何地方阻塞.

join()方法类似于get()。唯一的区别是,如果基础CompletableFuture异常完成,它将引发未经检查的异常

现在,计算包含关键字的网页数量

// 计算具有“CompletableFuture”关键字的网页的数量
CompletableFuture<Long> countFuture = allPageContentsFuture.thenApply(pageContents -> {
    return pageContents.stream()
            .filter(pageContent -> pageContent.contains("CompletableFuture"))
            .count();
});

System.out.println("具有CompletableFuture关键字的网页数:" + countFuture.get());
复制代码

4.3.3.2.CompletableFuture.anyOf()

顾名思义,CompletableFuture.anyOf()返回一个新的CompletableFuture,当给定的CompletableFuture中的任何一个完成时,新的CompletableFuture完成并具有相同的结果。

考虑一下例子

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
       throw new IllegalStateException(e);
    }
    return "Future1的结果";
});

CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
       throw new IllegalStateException(e);
    }
    return "Future2的结果";
});

CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> {
    try {
        TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
       throw new IllegalStateException(e);
    }
    return "Future3的结果";
});

CompletableFuture<Object> anyOfFuture = CompletableFuture.anyOf(future1, future2, future3);

System.out.println(anyOfFuture.get()); // 输出Future2的结果
复制代码

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

CompletableFuture.anyOf()接受一个Future变量,并返回CompletableFuture <Object>。 CompletableFuture.anyOf()的问题在于,如果您拥有返回不同类型结果的CompletableFuture,那么您将不知道最终CompletableFuture的类型。

Java并发工具类CompletableFuture教程示例

5.CompletableFuture异常处理

我们探讨了如何创建CompletableFuture,对其进行转换以及组合多个CompletableFuture。 现在,让我们了解发生任何问题时该怎么办。 首先,让我们了解错误如何在回调链中传播。

考虑以下CompletableFuture回调链

CompletableFuture.supplyAsync(() -> {
	// Code which might throw an exception
	return "Some result";
}).thenApply(result -> {
	return "processed result";
}).thenApply(result -> {
	return "result after further processing";
}).thenAccept(result -> {
	// do something with the final result
});
复制代码

如果原始的supplyAsync()任务中发生错误,则将不调用thenApply()回调,并且将在发生异常的情况下解决future。 如果在第一个thenApply()回调中发生错误,则不会调用第二个和第三个回调,并且将在发生异常的情况下解决将来的问题,依此类推.

5.1.使用exceptionally()回调处理异常

exceptionally()回调使您有机会从原始Future产生的错误中恢复。 您可以在此处记录异常并返回默认值。

Integer age = -1;

CompletableFuture<String> maturityFuture = CompletableFuture.supplyAsync(() -> {
    if(age < 0) {
        throw new IllegalArgumentException("年龄不可能为负数");
    }
    if(age > 18) {
        return "成年人";
    } else {
        return "未成年人";
    }
}).exceptionally(ex -> {
    System.out.println("我们得到异常:" + ex.getMessage());
    return "Unknown!";
});

System.out.println(maturityFuture.get());
复制代码

请注意,如果您处理一次,该错误将不会在回调链中进一步传播。

5.2.使用通用handle()方法处理异常

API还提供了一种更通用的方法handle()从异常中恢复。称为是否发生异常。

Integer age = -1;

CompletableFuture<String> maturityFuture = CompletableFuture.supplyAsync(() -> {
    if(age < 0) {
        throw new IllegalArgumentException("年龄不可能为负数");
    }
    if(age > 18) {
        return "成年人";
    } else {
        return "未成年人";
    }
}).handle((res, ex) -> {
    if(ex != null) {
        System.out.println("我们得到异常:" + ex.getMessage());
        return "Unknown!";
    }
    return res;
});

System.out.println(maturityFuture.get())
复制代码

如果发生异常,则res参数将为null,否则ex参数将为null。

6.总结

在本教程中,我们探讨了CompletableFuture API的最有用和最重要的概念。当然CompletableFuture还有很多没有介绍到的方法,希望大家自己慢慢研读,感谢您的阅读, 希望这篇博文对您有所帮助。

Java并发工具类CompletableFuture教程示例

猜你喜欢

转载自blog.csdn.net/a159357445566/article/details/112480401