简介
本章节的内容:
1、创建异步计算并获取计算结果;
2、使用非阻塞操作提升吞吐量;
3、设计和实现异步API;
同步API只是对传统方法调用的另一种称呼:你调用了某个方法,调用方在被调用方运行的过程会等待,被调用方法运行结束返回,调用方取得被调用方的返回值并继续运行。即使调用方法和被调用方在不同的线程运行,调用方还是需要等待被调用方结束后才能继续运行,这就是阻塞式调用的由来。
4、如何以异步的方式使用同步的API;
异步API会直接返回,或者至少在被调用方计算完成之前,将他剩余的计算任务交给另一个线程去做,该线程和调用方是异步的——这就是非阻塞式调用的由来。执行剩余计算任务的线程会将他的计算结果返回给调用方。返回的方式要么是通过回调函数,要么是由调用方再次执行一个“等待,直到计算完成(Future的get方法)”的方法调用。这种方式的计算在I/O系统程序设计中非常常见。
5、如何对两个或多个异步操作进行流水线和合并操作;
6、如何处理异步操作的完成状态。
1、Future接口
Future接口在Java5中被引入,设计初衷是对将来某个时刻会发生的结果进行建模。他建模了一种异步计算,返回一个执行运算结果的引用,当运算结束后,这个引用被返回给调用方。在Future中触发那些潜在耗时的操作吧调用线程解放出来,让他能继续执行其他有价值的工作,不再需要呆呆等待耗时的操作完成。
打个比方,你去一些快餐店吃饭,付钱之后,他不会让你在排队窗口那里呆呆的等。而是给你一张号码牌,告诉你什么时候你的饭菜会做好。做饭的同时,你可以做其他的事情,和同事坐在餐桌边聊聊今年为什么不涨工资种种。等到饭做好了,你再去取饭。
Future的另一个有点是他比更底层的Thread更易用。要使用Future,通常只需要将耗时的操作封装在一个Callable对象中,再将它提交给ExecutorService就行了。例如
package ch11;
import java.util.concurrent.*;
public class FutureTest {
public static void main(String[] args) {
// 创建ExecutorService,通过他你可以像线程池提交任务
ExecutorService executorService = Executors.newCachedThreadPool();
// 提交任务到线程池中异步执行;
// 1.提交的是一个Callable(有返回值)或Raunable(无返回值)对象;
// 2.你的主要任务应该是写在Callable对象的call方法之中;
// 3.泛型参数即为计算结果的类型;
// 4.该执行过程是异步的,也就是说,此处的代码不会产生阻塞,你可以在之后的代码中运行其他的操作;
Future<Integer> future = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() {
Integer result = 0;
System.out.println("Run some compute that can use more time...");
return result;
}
});
// 异步操作进行的同时,你可以做其他事情
System.out.println("Do other things...");
// 获取异步操作的结果,此处会产生阻塞,但可以设置等待时间
try {
// V get(long timeout, TimeUnit unit)
// 获取异步操作的结果,如果最终被阻塞,无法得到结果,那么将会
// 在对坐等待1秒之后退出。
future.get(1,TimeUnit.SECONDS);
} catch (InterruptedException e) {
// 当前线程在等待过程中被中断
e.printStackTrace();
} catch (ExecutionException e) {
// 计算抛出一个异常
e.printStackTrace();
} catch (TimeoutException e) {
// 超时异常
e.printStackTrace();
}
}
}
如果你已经运行到没有异步操作的结果就无法在进行下一步操作的时候,就可以考虑调用Future的get方法去获取操作的结果。如果操作已经完成该方法会立即得到操作结果,否则他会阻塞你的线程,直到操作完成,返回相应的结果。
但是无参的get方法是没有超时参数的,这可能会导致线程的永久阻塞,因此,推荐使用带有超时参数的get方法。除非你对你的程序有着“不可能计算出现问题”的自信。
1.1、Future接口的局限性
很明显,如果有多个Future在程序中出现,那么我们很难表述Future之间的依赖性。从文字上虽然可以这样表述:“当长时间计算任务完成时,请将该计算的结果通知到另一个长时间运行的计算任务,这两个计算任务都完成后,将计算的结果与另一个查询操作结果合并”。但是,使用Future中提供的方法完成这样的操作又是另一回事儿。这也是我们需要更具描述能力的特性的原因,比如:
1、两个两个异步计算大家的结果合并为一个 —— 这两个异步计算之间相互独立,同时第二个有依赖于第一个的结果;
2、等待Future集合中的所有任务都完成;
3、仅等待Future集合中最快结束的任务完成(有可能因为他们试图通过不同的方式计算同一个值);
4、通过编程方式完成一个Future任务的执行(即以手工设定异步操作的方式)
5、应对Future的完成事件。(即当Future的完成事件发生时会受到通知,并能使用Future计算的结果进行下一步的操作,不只是简单的橘色等待操作的结果)。
1.2、使用CompletableFuture构建异步应用
接下来我们会使用CompletableFuture构建异步应用,该应用大致就是一个获取顾客服务、商店打折价等。
2、实现异步API
如果您开发过集成式的应用程序,会深有感悟,很多获取结果的操作通常不是我们练习的时候信手捏来的值,而是通过各种外部服务获取,例如:
- 数据库访问
- ES访问
- 分布式文件系统访问
- 复杂算法推导
等等,当目标查询量、数据集过大时,这通常是一个耗时的操作。由于我们的重点不在这里,因此解析来会使用delay方法模拟这个耗时的过程,从而体现出使用异步任务相比较传统的同步方法的优势。delay方法如下所示,其实就是将当前线程休眠一段时间
public static void delay(){
try{
// sleep 1 sec.
Thread.sleep(1000L);
}catch(InterruptedExcetion e){
throw new RuntimeException(e);
}
}
接下来我们编写一个同步方法的应用方法,该方法用于查询一件商品的价格
package ch11;
import java.util.Random;
/**
* 商店类
*/
public class Shop {
private Random random;
private String shopName;
public Shop(String shopName) {
this.shopName = shopName;
this.random = new Random();
}
// 获取商店名称
public String getShopName() {
return shopName;
}
/**
* 模拟延迟操作
*/
public static void delay(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("sleep now thread error:" + e.getMessage());
e.printStackTrace();
}
}
public double getPrice(String product){
return calculatePrice(product);
}
/**
* 通过商品的前两个字符和一个随机数做乘积获得价格
* @param product
* @return
*/
private double calculatePrice(String product){
// delay times.
delay();
return random.nextDouble() * product.charAt(0) + product.charAt(1);
}
}
记住,delay模拟的是一个耗时的过程。很明显,这个类的使用者每次调用getPrice方法时,都会为的古代这个同步事件完成而等待1秒钟。这是无法接收的,尤其是考虑到价格查询器对网络中的所有的商店都重复这样的操作。
因此,我们希望使用异步的API重写这段代码。
2.1、将同步方法转换为异步方法
使用我们提到的Future转换方法
public Future<Double> getPriceAsync(String product){
...
}
Future可以理解为一个暂时还不可知道结果的处理器,这个结果在计算完成后,可以通过调用该Future对象的get方法取得。
因为这样的设计,getPriceAsync可以立即返回,调用线程可以有机会在同一时间去执行其他的有价值的计算任务。
新的CompleteableFuture提供了很多的方法,支持我们执行各种各样的操作。例如:
public Future<Double> getPriceAsync(String product){
// 创建一个CompletableFuture对象,他会包含计算的结果
CompletableFuture<Double> future = new CompletableFuture<>();
// 在另一个线程中以异步的方法执行计算
new Thread(() -> {
double price = calculatePrice(product);
// 需长时间计算的任务结束并得出结果时,设置Future的返回值。
future.complete(price);
}).start();
// 无需等待结果,直接返回Future对象
return future;
}
接下来我们测试该异步任务的表现
package ch11;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
public class CFutureTest {
public static void main(String[] args) {
Shop kfc = new Shop("KFC");
// 获得任意时间(纳秒)
long start = System.nanoTime();
Future<Double> hamburgerPriceFuture = kfc.getPriceAsync("hamburger");
long usedTimes = System.nanoTime() - start;
//
System.out.println("异步任务返回花费了" + usedTimes/1_000_000 + " 毫秒的时间;");
System.out.println("执行其他的任务...");
try {
Double price = hamburgerPriceFuture.get();
System.out.println("Price is " + price);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
usedTimes = System.nanoTime() - start;
System.out.println("获得异步任务结果花费了" + usedTimes/1_000_000 + "毫秒的时间");
/**
* 异步任务返回花费了55 毫秒的时间;
* 执行其他的任务...
* Price is 117.5786113274089
* 获得异步任务结果花费了1058毫秒的时间
*/
}
}
2.2、错误处理
如果价格计算的过程中出现了错误,我们该怎么办?
非常不幸的是,这种情况下,我们会得到一个相当严重的结果,用于提示错误的异常会被抑制(java core 异常章节有讲述抑制的概念)在视图计算商品价格的当前线程的范围内。最终会杀死该线程,而这会导致等待get方法返回结果的客户端永久阻塞。
为此,客户端需要使用重载版本的get方法。这样,在指定的时间内没有计算出结果时,提前获知超时消息。
但这也有一个问题,那就是我们无法获知到底发生了什么错误,这对于应用程序开发者来说是一个很严重的问题。为了让客户端知道发生了什么错误,我们可以使用CompletableFuture的completeExceptionally方法将导致CompletableFuture内发生的问题异常抛出。
基于此,我们来改写之前的代码(新增一个优化方法)
public Future<Double> getPriceAsync2(String product){
CompletableFuture<Double> future = new CompletableFuture<>();
new Thread(() -> {
try{
double price = calculatePrice(product);
// 如果计算正常结束,则完成complete操作并设置商品价格;
future.complete(price);
}catch (Exception e){
// 否则,抛出导致失败的异常,完成这次Future操作。
future.completeExceptionally(e);
}
}).start();
return future;
}
我们可以传入一个空的字符串作为商品名称参数来求取结果,显然会发生数组越界的异常。
Future<Double> hamburgerPriceFuture = kfc.getPriceAsync2("");
如果我们调用之前的getPriceAsync
方法,此时程序会被阻塞。
但是如果我们调用getPriceAsync2
则会得到异常的原因,以及主线程及时的终止
异步任务返回花费了55 毫秒的时间;
执行其他的任务...
获得异步任务结果花费了1057毫秒的时间
java.util.concurrent.ExecutionException: java.lang.StringIndexOutOfBoundsException: String index out of range: 0
at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:357)
at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1895)
at ch11.CFutureTest.main(CFutureTest.java:23)
Caused by: java.lang.StringIndexOutOfBoundsException: String index out of range: 0
at java.lang.String.charAt(String.java:658)
at ch11.Shop.calculatePrice(Shop.java:76)
at ch11.Shop.lambda$getPriceAsync2$1(Shop.java:57)
at java.lang.Thread.run(Thread.java:745)
Process finished with exit code 0
2.3、更优雅的编码方式
前面调用CompletableFutre的过程中,我们会明显的感觉到调用比较繁琐。其实还有优雅的创建、调用方式。即使用工厂方式取代之前的创建方式,我们改写之前的方法:
public Future<Double> getPriceAsync3(String product){
return CompletableFuture.supplyAsync(() -> calculatePrice(product));
}
supplyAsync接受一个生产者作为参数,返回一个CompletableFuture对象,该对象是完成异步执行后会读取调用生产者方法的返回值。
需要注意的是,该使用方式的效果和我们提供了错误管理机制的getPriceAsync2
是一样的,很明显,他优雅的许多。
3、让代码免受阻塞之苦
现在我们解决了查询一个商店的需求了,但新的需求来了,需要我们查询一个列表里面的商店对于指定的商品,到底哪一家的更便宜。
显然,这需要我们分别取访问所有商店提供的查询指定商品价格的API。
假设现在有5家商店:
List<Shop> shops = Arrays.asList(new Shop("shop1"),
new Shop("shop2"),
new Shop("shop3"),
new Shop("shop4"),
new Shop("shop5"));
针对该问题,我们来了三位参赛者A、B和C。他们提出了各自的解决方案,我们分别来看看他们的方案以及效果如何。
3.1、前两位参赛者:顺序流、并行流
A为我们提出了一个非常直接的解决方案,即使用java8的顺序流,我们来看看其方案的源码:
package ch11;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
public class CFutureTest2 {
public static void main(String[] args) {
String product = "hamburger";
List<Shop> shops = Arrays.asList(new Shop("shop1"),
new Shop("shop2"),
new Shop("shop3"),
new Shop("shop4"),
new Shop("shop5"));
long start = System.nanoTime();
List<String> list = shops.stream().map(shop -> {
return String.format("%s 的对应的价格为 %.2f", shop.getShopName(), shop.getPrice(product));
}).collect(Collectors.toList());
System.out.println(list);
System.out.println("=== 执行效果 ===");
System.out.println("该方式共花费时间(毫秒): " + (System.nanoTime() - start)/1_000_000);
/**
* [shop1 的对应的价格为 145.81, shop2 的对应的价格为 136.21, shop3 的对应的价格为 191.72, shop4 的对应的价格为 158.95, shop5 的对应的价格为 152.92]
* === 执行效果 ===
* 该方式共花费时间(毫秒): 5110
*/
}
}
因为是顺序执行的原因,因此5s+的时间大概是顺序的从这5家商店中依次获取商品价格所花费的时间。
我们的第一位方案提出者A,完成需求花费时间大约为5110毫秒。
接下来,B非常的志得意满,对A的结果嗤之以鼻。他说,这种情况完全可以使用java 8的并行流,A呐,既然学过java 8,不会不知道这东西吧,只需要修改你的stream方法为parallelStream就可以得到意想不到的结果了:
List<String> list = shops.parallelStream().map(shop -> {
return String.format("%s 的对应的价格为 %.2f", shop.getShopName(), shop.getPrice(product));
}).collect(Collectors.toList());
/**
* [shop1 的对应的价格为 176.59, shop2 的对应的价格为 184.50, shop3 的对应的价格为 141.36, shop4 的对应的价格为 143.61, shop5 的对应的价格为 131.53]
* === 执行效果 ===
* 该方式共花费时间(毫秒): 2098
*/
取得了不错的效果,只花费了2098毫秒。看来B并不是装腔作势,因为对5个商店的查询采取了并行操作,足足比之前的顺序流快了不止一倍的时间。
我们的第二位方案提出者B,花费时间为2098毫秒。
由于价格是随机生成的,因此显然和之前的例子价格会有所不同。但这不是重点。
问题来了,我们还可以做得更好吗?C会带来什么样的解决方案了,我们下一节拭目以待。
3.2、第三位参赛者:使用CompletableFuture发起异步请求
C非常赞成并行的处理方式,不过他觉得,B的这种方式有点太过极端,而且应对一些比较特殊的情况无法优雅的处理,甚至无法满足需求。
我们还可以做得更好。可以使用CompletableFuture,再进一步的对查询方法进行优化。
我们不妨看看C的源码,看看是否能取得更好的成绩。
List<String> list = shops.stream().map(shop -> {
return CompletableFuture.supplyAsync(() -> {
return String.format("%s 的对应的价格为 %.2f", shop.getShopName(), shop.getPrice(product));
});
}).map(CompletableFuture::join).collect(Collectors.toList());
System.out.println(list);
// 该方式共花费时间(毫秒): 5095
join和get的效果基本一致,唯一的区别在于,使用join方法不会抛出任何检测到的异常,因此我们不必在使用try/catch语句。
5秒了,但C立马解释说,之所以花费这么多时间是由于stream的延迟特性导致的,因此,需要使用两个不同的流水线,而不是一个,一个流水线会导致发现不同商家的请求只能以同步、顺序执行的方式才会成功。因此,每个创建CompletableFuture对象只能在前一个操作结束之后执行查询指定商家的动作、通知join方法返回计算结果。
为了避免这种情况的发生,我们应该使用两个不同的map流水线。如下所示:
List<CompletableFuture<String>> futures = shops.stream().map(shop -> {
return CompletableFuture.supplyAsync(() -> {
return String.format("%s 的对应的价格为 %.2f", shop.getShopName(), shop.getPrice(product));
});
}).collect(Collectors.toList());
List<String> list = futures.stream().map(CompletableFuture::join).collect(Collectors.toList());
// === 执行效果 ===
// 该方式共花费时间(毫秒): 2133
2133毫秒,期待着得到不错成绩的我们觉得有些失望。这意味着C的解决方案也不见得比B好到哪里去,甚至显得有些昂长。
C真的不比B好吗?C的做法很多余吗?我们接着往下看。
3.3、寻求更好的方案
我们知道,并行处理任务数,一般是和CPU的核数对等的,例如,我当前的这台电脑的核数是4
System.out.print("CPU核数:" + Runtime.getRuntime().availableProcessors());
// CPU个数:4
这也就是意味着,从1开始计数,每当我的商店数增加4个,那么针对上述的方案B和C,执行时间都会增加一秒。因为B和C内部采用的都是同样的通用线程池,默认都是用固定数目的线程,具体数目取决于上述测试代码,即Runtime.getRuntime().availableProcessors()
的返回值。然而CompletableFuture
具有一定的优势,因为他允许你对执行器(Executor)进行配置,尤其是线程池的大小,让他以更适合应用需求的方式进行配置,满足程序的要求,这些,我们的并行流是无法提供的。
3.4、使用定制的执行器
前面提到,我们可以创建一个配有线程池的执行器,线程池中线程的数目取决于你预计你的应用需要处理的符合,但是你该如何选择合适的线程数目呢?
线程数目的多少是非常讲究的?设置的过小,如我们之前所面临的问题一样,无法充分的利用处理器的性能,如果业务比较频繁,还会导致过多的任务面临排队过长的状况;相反,设置的过大,会导致他们竞争稀缺的处理器和内存资源,浪费大量的时间用在上下文的切换上。
估算线程池大小的公式 $$ N = N_{cpu} * U_{cpu} * (1 + \frac{W}{C}) $$ 其中:
- $N_{cpu}$:是CPU的核数,例如我们上面看到的4核;
- $U_{cpu}$: 是期望的CPU利用率
- $\frac{W}{C}$ 是等待时间(wait time)与计算时间(compute time)的比率
针对该公式,显然我们的计算时间W/C约为100倍,假设需要CPU利用率为100%,我们显然要创建包含400个线程的线程池。
实际操作中,其实我们只需商店数目的线程数就可以了。
不过,为了避免商店数目过多,我们可以给其设定一个上限值100。如下所示:
final ExecutorService executorService = Executors.newFixedThreadPool(Math.min(shops.size(), 100), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
// 设置为守护进程
t.setDaemon(true);
return t;
}
});
List<CompletableFuture<String>> futures = shops.stream().map(shop -> {
// public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,Executor executor)
return CompletableFuture.supplyAsync(() -> {
return String.format("%s 的对应的价格为 %.2f", shop.getShopName(), shop.getPrice(product));
},executorService);
}).collect(Collectors.toList());
也就是说,我们将执行器作为第二个参数传递给了supplyAsync方法。
改进之后,看一下时间的花费
=== 执行效果 ===
该方式共花费时间(毫秒): 1091
只需1091毫秒。
3.5、使用并行流还是CompletableFuture?
目前为止,我们知道了对集合进行并行计算的两种方式:
-
转化为并行流,利用map展开工作;
-
枚举集合的每一个元素,创建新的线程,在CompletableFuture内对其进行操作。
后者提供了更好的灵活性,支持我们调整线程池的大小,也就是以为这,确保整体的计算不会因为线程都在等待I/P而发生阻塞。
那么,在实际情况中该选择哪一种呢?建议如下:
-
如果进行的是计算密集型的操作,并且没有I/O,那么推荐使用stream接口。因为实现简单,同时效率也是最高的。
-
如果并行的工作单元还涉及到等到I/O的操作,例如网络连接等,那么使用CompletableFuture更好,你可以像我们讨论的那样,依据等待/计算,或者W/C的比率设定需要使用的线程数。这种情况下不适用并行流的原因还有一个,那就是处理刘的流水线中如果发生I/O等待,流的延迟特性会让我们很难判断到底什么时候触发了等待。
4、对多个异步任务进行流水线操作
接下来我们定义一个折扣服务,该折扣服务提供了五个不同的折扣代码,每个折扣代码对应不同的折扣率。
具体代码如下所示:
package ch12;
public class Discount {
public enum Code{
NONE(0),SILVER(5),GOLD(10),PLATINUM(15),DIAMOND(20);
private final int percentage;
Code(int percentage){
this.percentage = percentage;
}
}
// other code...
}
接下来,我们修改之前的getPrice方法,将其返回的价格形式改变
public String getPrice(String product){
double price = calculatePrice(product);
// 从五中折扣中随即选取一种
Discount.Code code = Discount.Code.values()[random.nextInt(Discount.Code.values().length)];
// shopName:price:discountCode
return String.format("%s:%.2f:%s",shopName,price,code);
}
4.1、实现折扣服务
我们从商店的getPrice方法中可以获得shopName:price:discountCode
,接下来我们需要知道具体的折扣价格,因此编写一个类来进行解析:
package ch12;
public class Quote {
private final String shopName;
private final double price;
private final Discount.Code discountCode;
public Quote(String shopName, double price, Discount.Code discountCode) {
this.shopName = shopName;
this.price = price;
this.discountCode = discountCode;
}
/**
* 解析方法
* @param str shopName:price:discountCode
* @return
*/
public static Quote parse(String str){
String[] tokens = str.split(":");
return new Quote(tokens[0],Double.parseDouble(tokens[1]),Discount.Code.valueOf(tokens[2]));
}
public String getShopName() {
return shopName;
}
public double getPrice() {
return price;
}
public Discount.Code getDiscountCode() {
return discountCode;
}
}
另外,我们还在Discount类中添加了一个方法,他接收一个Quote对象,返回一个字符串,表示生成该Quote的商店中的折扣价格:
/**
* 将折扣应用于商品最初的原始价格
* @param q
* @return
*/
public static String applyDiscount(Quote q){
return q.getShopName() + "的价格是:" + Discount.apply(q.getPrice(),q.getDiscountCode());
}
/**
* 获取折扣之后的价格,模拟了延迟
* @param price
* @param code
* @return
*/
private static double apply(double price, Code code){
delay();
return price * (100 - code.percentage)/100;
}
/**
* 模拟延迟操作
*/
public static void delay(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("sleep now thread error:" + e.getMessage());
e.printStackTrace();
}
}
4.2、使用折扣服务
由于Discount也是一种远程服务,因此我们在之前他的时候模拟了一秒钟的延迟时间。接下来我们使用和之前一样的几种方式来使用该服务。
1、同步执行:
package ch12;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Test1 {
List<Shop> shops = Arrays.asList(new Shop("shop1"),
new Shop("shop2"),
new Shop("shop3"),
new Shop("shop4"));
public static void main(String[] args) {
long start = System.nanoTime();
System.out.println(new Test1().getPrices("sss"));
System.out.println("花费时间为(毫秒): " + (System.nanoTime() - start) / 1_000_000);
/**
* [shop1的价格是:190.06850000000003, shop2的价格是:218.77, shop3的价格是:134.43, shop4的价格是:202.89149999999998]
* 花费时间为(毫秒): 8174
*/
}
public List<String> getPrices(String product){
return shops.stream().map(shop -> shop.getPrice(product))
.map(Quote::parse)
.map(Discount::applyDiscount)
.collect(Collectors.toList());
}
}
差不多8秒的时间,在预料之内。
2、使用并行流优化
很明显,当前4个商店,我们的电脑拥有4个核,因此,并行流操作花费的时间应该是1+1=2S左右,我们修改代码如下:
public static void main(String[] args) {
long start = System.nanoTime();
System.out.println(new Test2().getPrices("sss"));
System.out.println("花费时间为(毫秒): " + (System.nanoTime() - start) / 1_000_000);
/**
* [shop1的价格是:206.37, shop2的价格是:224.38, shop3的价格是:160.1315, shop4的价格是:178.02900000000002]
* 花费时间为(毫秒): 2105
*/
}
和预估的差不多。但同样的,并行流的方式无法控制线程数量,因此会在商店数目上升之后变得力不从心,也就是扩展性并不是很好。
4.3、构造同步和异步操作
接下来使用Completable提供的特性改造代码。
public List<String> getPrices(String product){
ExecutorService executor = Executors.newFixedThreadPool(shops.size(), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
}
});
List<CompletableFuture<String>> priceFuture = shops.stream()
// 以异步方式取得每个shop中指定产品的shopName:xxx
.map(shop -> CompletableFuture.supplyAsync(() -> shop.getPrice(product),executor))
// 然后解析返回的串
.map(f -> f.thenApply(Quote::parse))
// 使用另一个异步任务构造期望的future,申请折扣
.map(f -> f.thenCompose(quote -> CompletableFuture.supplyAsync(() -> Discount.applyDiscount(quote),executor)))
.collect(Collectors.toList());
// 等待流中的所有Future执行完毕,并提取各自的返回值
return priceFuture.stream().map(CompletableFuture::join).collect(Collectors.toList());
/**
* [shop1的价格是:111.682, shop2的价格是:164.6875, shop3的价格是:97.20800000000001, shop4的价格是:147.77]
* 花费时间为(毫秒): 2115
*/
}
需要注意里面出现的两个新方法,thenApply以及thenCompose:
-
thenApply()
是返回的是CompletableFuture
类型:它的功能相当于将CompletableFuture<T>
转换成CompletableFuture<U>
。 -
thenCompose()
用来连接两个CompletableFuture
,返回值是新的CompletableFuture
。
也就是说,thenApply() 转换的是泛型中的类型,是同一个CompletableFuture; thenCompose()用来连接两个CompletableFuture,是生成一个新的CompletableFuture。也就是说,在thenCompose()
中一般会设计到CompletableFuture的创建代码。新生成的CompletableFuture使用先前的CompletableFuture作为输入。
4.4、连接两个CompletableFuture对象(不论是否存在依赖)
更常见的情况是将两个完全不相干的CompletableFuture对象的结果整合起来(thenCombine)。
public <U,V> CompletableFuture<V> thenCombine(
CompletionStage<? extends U> other,
BiFunction<? super T,? super U,? extends V> fn)
例如,如果某个商店返回的价格是以欧元为单位的,我们的需求是得到美元的结果,那么,我们同时还需要去一个提供汇率的服务获取美元和欧元之间的汇率,最后,将这两个结果进行组合(thenCombine),从而得到最终的以美元为单位的结果。
package ch11;
import ch12.ExchangeService;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
public class CFutureTest5 {
public static void main(String[] args) {
Shop shop = new Shop("KFC");
// 获得任意时间(纳秒)
long start = System.nanoTime();
CompletableFuture<Double> future = CompletableFuture
.supplyAsync(() -> shop.getPrice("sss"))
// 组合获取汇率的调用结果,第二个参数是 BiFunction类型,结合两者的结果,最终返回最后的答案。
.thenCombine(CompletableFuture.supplyAsync(() -> ExchangeService.getRate("usa", "europe")), (price, rate) -> price * rate);
try {
System.out.printf("结果为:%.2f\r\n" ,future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("获得异步任务结果花费了" + (System.nanoTime() - start)/1_000_000 + "毫秒的时间");
}
}
为了演示,我们添加了一个计算汇率的简便方法
package ch12;
public class ExchangeService {
public static double getRate(String firstCountry, String secondContry){
// 1欧元=1.1233美元
// 其实这里应该模拟一下延迟的:delay()
return 1.1233;
}
}
当然,你也可以使用theCombineAsync方法来启动一个新的线程执行结果的整合操作,但是这个计算显然很快速,因此没有必要在开启一个新的线程。
4.5、Future和CompletableFuture的回顾
现在我们已经可以说,CompletableFuture相比较直接使用Java 7的Future的优势。很多时候,我们如果采用java 7去编写相同的案例,情况会变得复杂很多。
但是现在还有一个小小的问题,那就是我们调用get或者join时还说会造成线程阻塞,直到CompletableFuture完成后才会继续往下执行。
而接下来,我们针对这个问题,来学习一下CompletableFuture的completion事件。
5、completion事件
全部完成
public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs) {
一个完成即可
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs) {
thenAccept:在返回结果上应用一个回调操作,也就是说,该方法定义的操作会在CompletableFuture得到计算结果之后执行。
CompletableFuture<Void> thenAccept(Consumer<? super T> action)
由于thenAccept已经定义了对返回结果的操作,一旦计算到结果,thenAccept返回的就是一个CompletableFuture<Void>
类型的结果,这对于我们没有什么作用。
CompletableFuture[] completableFutures = shops.stream()
.map(shop -> CompletableFuture.supplyAsync(() -> String.format("%s 的对应的价格为 %.2f", shop.getShopName(), shop.getPrice(product)),
executorService))
.map(f -> f.thenAccept(s -> System.out.println(s)))
.toArray(size -> new CompletableFuture[size]);
System.out.println("===");
CompletableFuture.anyOf(completableFutures).join();
/**
* ===
* shop5 的对应的价格为 186.41
*/
注意delay的时间设置成为了一个随机的值。这样我们使用anyOf之后,则该计算会在第一个完成的任务之后直接不再等待,往下执行。
总结
这一章节,我们学习到的内容如下:
1、执行比较耗时的操作时,尤其是那些依赖一个或多个远程服务的操作,使用异步任务可以改善程序的性能,加快程序的响应速度;
2、你应该尽可能的为客户提供异步API,使用CompletableFuture类提供的特性,能够轻松地实现这一目标;
3、CompetableFuture类还提供了异常管理的机制,让我们有机会抛出或者管理异步任务执行时产生的异常;
4、将同步api的调用封装到一个CompletableFuture中,你能够以异步的方式使用其结果;
5、如果异步任务之间相互独立,或者他们之间某一些的结果是另一些的输入,你可以将这些异步任务构造或者合并成一个;
6、你可以为CompletableFuture注册一个回调函数,在Future执行完毕或者他们计算的结果可用时,针对性的执行一些程序;
7、你可以决定在什么时候结束程序的运行,是等待由CompletableFuture对象构成的列表中所有的对象都执行完毕,还是只要其中任何一个首先完成就终止程序的运行。