CompletableFuture:提升Java的并发性


前言

我对线程、并发、异步还没有清晰的认识,在书的15、16和17章讲了Java的并发,第17章讲了Java9才有的FlowApi,暂时不记录,也不会完全按照书上的内容写,相当于筛选一些我想记的信息。


一.什么是并发?什么是并行?

首先,CPU在处理多个任务时是通过调度和上下文切换实现的,只不过切换的速度非常快,所以看起来是多个任务在同时进行,这就是并发,在一个处理器上处理多个任务,给你一个同时运行的假象。
而并行是在多台处理器上同时处理多个任务,多核处理器可以实现。


二.Java8并发的背景

近年来(指2016年之前),程序员受到两股潮流的影响。
一是硬件平台的升级对程序运行产生了影响,由于多核处理器的出现,我们可以把一个大的任务分成很多小的子任务,让它们独立并发运行于多个核上,比如fork/join框架和并行流,其效率甚至比使用线程还高。
二是互联网应用的新潮流,使用微服务架构的应用越来越多,应用被分为很多小型服务,彼此之间互相通信,我们开发个应用可以去调用某些企业提供的API,比如你需要一个翻译的功能,那么你完全可以去申请使用谷歌翻译的API。
这两个潮流是相辅相成的,当你使用第三方API,请求并等待外部服务的相应时,其他的任务肯定是不能停止的。

那么并行是怎么做的呢,书的第7章介绍了fork/join框架和并行流,把任务切分为子任务,分配到不同的核、CPU或机器上并行运行。
那我要是去做并发呢,我们的任务是当等待远程服务响应时,尽可能让核都忙碌起来,从而最大化应用的吞吐量,避免浪费计算资源。
为了解决这个问题,Java8提供了Future接口,还有CompletableFuture接口。


三.为了支持并发而不断演进的Java

1. 线程以及更高层的抽象

线程是什么?进程是什么?
一个单CPU的计算机支持多个用户,因为操作系统为每个用户分配了一个进程,这些进程由独立的虚拟地址空间。
一个进程可以请求操作系统给它一个或多个线程——它们跟主进程之间共享地址空间,因此可以并发执行并相互协调。

那么在一个多核的环境中呢?如果你的程序没有使用多线程,那就只能用到一个核。
在以前,我们想用多线程,就得声明使用,现在我们可以直接用Stream:

int sum = Arrays.stream(stats).parallel().sum;

这里想说的意思是,对并行流的迭代是比显示使用线程更高级的概念。换句话说,流是一种对线程使用模式的抽象。

还有java.util.concurrent.RecurisiveTask,它是对fork/join的抽象,写起来更方便。
在学习更多的线程抽象方法之前,我们先来看看Java5引入的ExecutorService,以及这些方法的基础——线程池。

2.执行器和线程池

Java的线程直接访问系统的线程,所以有很多问题,比如创建删除线程的代价很大,线程数量是有上限的,因此Java5提供了线程池的方法。
Java的ExecutorService提供了接口,我们可以创建一个线程池:

ExecutorService newFixedThreadPool(int nThreads)

这个东西的作用就是管理线程,新创建的线程被放入线程池,有新任务请求时,按照先来先到的策略去线程池中选取为被使用的线程来执行任务,用完再放回去,这种方法可以以很低的成本向线程池提交上千个任务,同时保证硬件匹配的任务执行。

请注意这里的术语:程序员提供任务(可以是一个Runnable或者Callable),由线程负责执行。

在大多数情况下,用线程池比直接操纵线程要好,但是线程池也有问题:
(1)任务超出池的容量,就会排队等候,有阻塞式I/O任务时,或线程进入了睡眠,会有长时间的等待,程序的吞吐量大大降低;如果任务需要后续的任务响应,那就会死锁。
(2)通常情况下,Java从main返回前,会等待所有线程执行完毕,避免误杀线程。一个好习惯是,推出程序执行之前,确保关闭所有线程池。实战中经常用一个长时间运行的线程池管理需要运行的网络服务。Java也提供了Thread.setDaemon方法来控制这种行为。

3.非嵌套方法调用

让我们再来看看并发和并行的不同。
先来看看并行:

无论什么时候,任何任务(或者线程)在方法调用启动时,都会在其返回之前调用同一个方法。换句话说,线程创建以及与其匹配的join()在调用返回的嵌套方法调用中都以嵌套的方式成对出现。这种思想被称为严格fork/join

这是什么意思?
大概就是说fork/join是嵌套的方法调用,线程的创建和执行的方法是严格组合在一切的。
那就有非嵌套的方法:异步方法
异步方法中,用户调用方法创建的线程可能比调用方法的声明周期还长,意思就是这个方法所派生出的任务会继续执行调用方法来完成工作。

不过异步方法也有弊端:
(1)子线程与执行方法调用的代码会并发执行,因此需要避免出现数据竞争的情况。
(2)怎么处理线程的结束和程序执行的结束?要么等待所有线程结束再结束程序,但如果有线程无法顺利结束就有问题;要么杀死有问题的线程,但这样可能会导致数据出问题。所以需要保证你能有效地追踪线程,且退出程序(包括线程的关闭)之前必须加入这些线程。

依据有没有执行setDaemon()方法,Java线程可以被划分为守护进程以及非守护进程。守护进程的线程在退出时就被终止,而从主程序返回的线程还得等待,直到所有非守护线程都终止了,应用才能推出程序。


四.同步和异步API

在前面我们学过Stream流,流使用内部迭代替换外部迭代(for循环),还可以用parallel()并发处理,程序员不用再去想着怎么自己处理线程了。
并行操作除了能给迭代带来优化,还有一个很重要的好处,那就是异步API。
让我们来看看:

int f(int x);
int g(int y);

首先,这两个函数签名都是同步API,因为它们物理上返回时,执行结果也一起返回了。
当我们调用这两个函数时,我们想要进行优化,减少运行的时间,一个做法是让它们并行执行,这样实际时间就取两个函数中较长的那个。
让我们来看看怎么实现:

int left, right;
Thread t1 = new Thread(() -> {left = f(x)});
Thread t2 = new Thread(() -> {right = g(x)});
t1.start();
t2.start();
t1.join();
t2.join();

我们可以进行优化,比如使用FutureExecutorService的线程池:

ExecutorService executorService = ExecutorService.newFixedThreadPool(2);
Future<Integer> y = executorService.submit(() -> f(x));
Future<Integer> z = executorService.submit(() -> f(x));
left = y.get();
right = z.get();
executorService.shutdoun();

想想Stream,我们用内部迭代替代了显示的外部迭代,而这里我们写的是显示调用线程,能不能不这么写?
答案是将同步API转化为异步API

注:由于书上这部分看的我摸不着头脑,所以跳过了
后面还有一些东西,也跳过了

先看一下后面16章给出的定义吧:

同步API其实只是对传统调用的另一种称呼:你调用了某个方法,调用方在方法执行的过程中会等待,被调用方执行结束返回,调用方取得被调用方的返回值并继续运行。即使调用方和被调用方可以在不同的线程中运行,调用方还是要等待被调用方返回,这就是阻塞式调用名字的由来。

这很好理解,再来看看异步API:

异步API在被调用方计算完成之前,将它剩余的计算任务交给另一个线程去做,该线程和调用方是异步的——这就是非阻塞调用的由来。

执行计算的线程会将结果返回给调用方。返回方式要么是通过回调函数,要么由调用方发起一个“等待,指导计算完成”的方法调用。

异步API不会在等着被调用方返回结果才继续执行,可以发起个请求,然后就干别的事去了,需要得到结果的时候,可以再发个请求,也可以提前设个回调函数返回。


五. 实现异步编程

1.使用Future接口

采用Future接口可以对异步计算进行建模,返回一个指向执行结果的引用,运算结束后,调用方可以通过该引用访问执行的结果。在Future中触发那些可能耗时的调用,可以将调用线程解放出来,让它们继续执行其他有价值的工作。

 // 使用Future以异步的方式执行一个耗时的操作。
 ExecutorService executorService = Executors.newCachedThreadPool();
 Future<Double> future = executorService.submit(new Callable<Double>() {
     @Override
     public Double call() throws Exception {
         return doSomeLongComputation();
     }
 });
 doSomethingElse(); // 异步操作进行时,可以干别的事
 try {
     Double result = future.get(1, TimeUnit.SECONDS); // 获取异步操作的结果 如果阻塞了,等待1秒
 }catch (Exception ee){
     // 计算抛出一个异常
 }catch (InterruptedException ie){
     // 当前线程在等待过程中被中断
 }catch (TimeoutException te){
     // 在Future对象完成前超过已过期
 }

![  ](https://img-blog.csdnimg.cn/18b98c60967b4f15bf4a4f4a5efb2aa0.png

这样的方式实现了异步运行,两个计算相互独立,但又伴随着依赖。
在这里,为了避免不返回结果导致的阻塞,使用get()方法返回,除此之外还有isDone()方法检测异步操作是否结束。
但是显然,这样的写法不够简洁。

2.使用CompletableFyture接口

public Future<Double> getPriceAsync(String product){ // 一个最佳价格查询方法,为了实现异步,将返回值写为Future<Double>
	CompletableFuture<Double> futurePrice = new CompletableFuture<>(); // 用来包含计算的结果
	new Thread(() -> {
				double price = calculatePrice(product); //一个要计算很久的方法
				futurePrice.complete(price);    // 对于需要长时间计算得到的结果,用CompletableFuture接收
			}).start();
			return futurePrice; // 直接返回,不用等待计算结束
}
//使用上述方法
Future<Double> futurePrice = getPriceAsync("apple");
//干点别的
doSomethingElse();
try {
	double price = futurePrice.get(); //获取结果,如果计算不出来,就会阻塞
}catch(Exception e){
	throw new RuntimeException(e);
}

异步API体现在getPriceAsync方法会直接返回,但实际计算的结果在请求时才会返回。
相比于Future,使用CompleFuture将异步操作的结果封装了起来。
显然,这段代码有阻塞的问题,怎么去管理异步任务执行时可能存在的问题呢?

public Future<Double> getPriceAsync(String product){ 
	CompletableFuture<Double> futurePrice = new CompletableFuture<>(); 
	new Thread(() -> {
				try{
					double price = calculatePrice(product); 
					futurePrice.complete(price);    
				}catch (Exception ex){
					futurePrice.completableExceptionally(ex);  // 抛出异常
			}).start();
			return futurePrice; 
}

好了,这下这段代码看起来十分臃肿,我们可以使用supplyAsync

public Future<Double> getPriceAsync(String product){ 
	return CompletableFuture.supplyAsync( () -> calculatePrice(product));
}

supplyAsync接收一个生产者方法,返回一个CompletableFuture对象,该对象完成异步执行后会读取生产者方法的返回值。
生产者方法交由ForkJoinPool池中的某个执行线程,你也可以使用supplyAsync的重载方法传入第二个参数指定不同执行线程的生产者方法。
supplyAsync方法中就有错误管理机制,避免阻塞情况。

3.CompletableFuture与并行流

public List<String> findPrice(String product) {
	// 根据商品名称从shop列表中获取该商品在不同shop的价格
	return shop.parallelStream() // 并行流
			   .map(shop -> String.format("%s price is %f", shop.getName(), shop.getPrice(product))) 
			   .collect(toList());
}
public List<String> findPrice(String product) {
	// 并发实现
	List<CompletableFuture<String>> priceFutures = shop.stream()
		.map(shop -> CompletableFuture.supplyAsync( 
		() -> String.format("%s price is %f", shop.getName(), shop.getPrice(product))))
		.collect(toList());
		
	return priceFutures.stream()
			.map(CompletableFuture::join)  // 获取结果 等待所有异步执行结束
			.collect(toList());
	/**
	get() 返回经过经检查的异常,可被捕获,自定义处理或者直接抛出。
	join() 会抛出未经检查的异常。
	*/
}

那么,使用并行流还是并发呢?

如果你进行的是计算密集型的操作,并且没有I/O,那么推荐使用Stream。
如果设计I/O操作(还有网络等待),那么使用并发灵活性更好。

4.定制执行器


private final Executor exrcutor = 
	// 创建一个由守护线程构成的线程池 线程数目为100和shop数中较小的值
	Executor.newFixedThreadPool(Math.min(shops.size(), 100),
								  (Runnable r) -> {
			Thread t = new Thread(r);
			t.setDaemon(true);
			return t;
		}
};

public Future<Double> getPriceAsync(String product){ 
	// 使用守护线程
	return CompletableFuture.supplyAsync(() -> calculatePrice(product), executor);
}

当一个普通线程在执行时,如果程序无法终止或者退出,最后剩下的线程会由于一直等待结果而阻塞,如果将其标记为守护线程,则意味着程序退出时它也会被回收。


六.对多个异步任务进行流水线操作

书上以一个例子来解释如何用,这里省略了一部分具体的功能实现,主要想关注于异步操作

public List<String> findPrices(String product){
	return shops.stream()
			.map(shop -> shop.getPrice(product))  // 获取shop名称+商品的价格+折扣率
			.map(Quotr::parse)                    // 转换成一个Quotr对象,包含shop名称,商品折扣前价格,折扣率
			.map(Discount::applyDiscount)		  // Discount是个远程服务,用来申请折扣,但有时间延迟,最后返回字符串
			.collect(toList());
}

显然,由于有延迟的Discount服务,使用stream流水线式的执行,必然导致很长的时间消耗。

1.构造同步和异步操作

使用CompletableFuture解决问题:

public List<String> findPrices(String product){
	List<CompletableFuture<String>> priceFutures =
			 shops.stream()
			.map(shop -> ComptableFuture.supplyAsync(            // 以异步方式获取 
					() -> shop.getPrice(product), executor)  	 // 使用Executor执行器
			.map(future -> future.thenApply(Quotr::parse))       // 使用thenApply 
			.map(future -> future.thenCompose(					 // 使用thenCompose 
				 		quote -> ComptableFuture.supplyAsync(
				 				() -> Discount.applyDiscount(quote), executor)		  
			.collect(toList());
	
	return priceFutures.stream().map(CompletablFuture::join).collect(toList);  // 返回结果
}

用图来解释发生了什么:
![![在这里插入图片描述](https://img-blog.csdnimg.cn/020a09fb6d4247068c9b68b46363f151.png](https://img-blog.csdnimg.cn/bb90da1cdf9442fc892ed93f44aa8862.png
1.Executor执行器提供了很多个线程来使用
2.thenApply是异步的,不会阻塞代码的执行
3.thenCompose允许你对两个CompletableFuture进行流水线操作,前一个的结果作为后一个的参数。在这里,前一个任务是shop.getPrice()和转化为Quote,后一个任务是将Quote传给Discount

2.整合两个CompletableFuture对象

当我们需要使用上一个任务的结果来执行下一个任务,我们可以使用thenCompose
但有的时候,我们需要将两个任务的结果合并计算,这时候我们可以使用thenCombine

Future<Double> futurePriceInUSD = 
	CompletableFuture.supplyAsync( () -> shop.getPrice(product)) // 获取价格
	.thenCombine(
		CompletableFuture.supplyAsync( () -> exchangeService.getRate(Money.EUR, Money.USD)), // 转换货币
		(price, rate) -> price * rate  // 合并结果
	);

相比于申请future的结果,写起来更方便了
注:thenComposethenCombine都有并发版本:thenComposeAsyncthenCombineAsync

3.添加超时

Future<Double> futurePriceInUSD = 
	CompletableFuture.supplyAsync( () -> shop.getPrice(product)) // 获取价格
	.thenCombine(
		CompletableFuture.supplyAsync( () -> exchangeService.getRate(Money.EUR, Money.USD)), // 转换货币
		(price, rate) -> price * rate  // 合并结果
	)
	.orTimeout(3, TimeUnit.SECONDS); // 如果无法在3秒内完成,就抛出一个TimeException超时异常

4.响应CompletableFuture的completion事件

在实际中,调用远程服务必然有延迟,那该如何优化呢
一个方法是避免List,使用流获取远程服务的结果

// 修改前面的findPrice,直接返回流
public Stream<CompletableFuture<String>> findPriceStream(String product){
	retrurn shop.stream()
			 shops.stream()
			.map(shop -> ComptableFuture.supplyAsync(          
					() -> shop.getPrice(product), executor)  	
			.map(future -> future.thenApply(Quotr::parse))       
			.map(future -> future.thenCompose(					 
				 		quote -> ComptableFuture.supplyAsync(
				 				() -> Discount.applyDiscount(quote), executor)));
}
// 然后,使用thenAccept
findPriceStream("HuaWei").map(f -> f.thenAccept(System.out::println));

thenAccept获取了CompletableFuture返回的结果,然后返回一个CompletableFuture<Void>,对于一个这样的对象,我们能做的事非常有限,只能等着它结束,为了更好地操作,可以把所有对象放入数组中。
注:thenAccept也有并发版本:thenAcceptAsync

CompletableFuture [] futures = findPriceStream("HuaWei")
				 .map(f -> f.thenAccept(System.out::println))
				 .toArray(size -> new CompletableFuture[size]);
// allOf等待所有CompletableFuture执行结束,返回一个CompletableFuture<Void>,再使用join()获取结果
CompletableFuture.allOf(futures).join();  

小结

1.执行耗时的操作,尤其是依赖远程服务的操作,使用异步可以改善性能
2.CompletableFuture也提供了异常机制
3.将同步API的调用写入CompletableFuture中,可以以异步的方式使用其结果
4.我们可以合并异步任务
5.我们可以控制什么时候结束程序的运行

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值