24. CompletableFuture:异步编程没那么难 - 并发工具类

异步化,是并行方案得以实施的基础,更深入地讲其实就是:利用多线程优化性能这个核心方案得以实施的基础

1. CompletableFuture的核心优势

利用CompletableFuture实现前面的泡茶程序。
分工,任务1负责洗水壶、烧开水,任务2负责洗茶壶、洗茶杯和拿茶叶,任务3负责泡茶。其中任务3要等待任务1和任务2都完成后才能开始。
在这里插入图片描述

public class MyTest3 {
	public static void main(String[] args) {
		//任务1 洗水壶、烧开水
		CompletableFuture<Void> f1 = CompletableFuture.runAsync(() -> {
			System.out.println("T1 洗水壶");
			sleep(1, TimeUnit.SECONDS);

			System.out.println("T1 烧开水");
			sleep(15, TimeUnit.SECONDS);
		});
		//任务2 烧茶壶、洗茶杯、拿茶叶
		CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> {
			System.out.println("T2:洗茶壶...");
			sleep(1, TimeUnit.SECONDS);

			System.out.println("T2:洗茶杯...");
			sleep(2, TimeUnit.SECONDS);

			System.out.println("T2:拿茶叶...");
			sleep(1, TimeUnit.SECONDS);
			return "龙井";
		});
		
		//任务3:任务1和任务2完成后执行:泡茶
		CompletableFuture<String> f3 = f1.thenCombine(f2, (__, tf)->{
		    System.out.println("T1:拿到茶叶:" + tf);
		    System.out.println("T1:泡茶...");
		    return "上茶:" + tf;
		  });
		
		//等待任务3执行结果
		System.out.println(f3.join());
	}

	private static void sleep(int t, TimeUnit u) {
		try {
			u.sleep(t);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

优势:

  • 无需手工维护线程,没有繁琐的手工维护线程的工作,给任务分配线程的工作也不需要我们关注;
  • 语义更清晰,例如 f3 = f1.thenCombine(f2, ()->{}) 能够清晰地表述“任务3要等待任务1和任务2都完成后才能开始”;
  • 代码更简练并且专注于业务逻辑,几乎所有代码都是业务逻辑相关的。

2. 创建CompletableFuture对象

/使用默认线程池
static CompletableFuture<Void>  runAsync(Runnable runnable)
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
//可以指定线程池  
static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)  
  • runAsync(Runnable runnable) 无返回值,supplyAsync(Supplier supplier)有返回值;
  • 前两个方法和后两个方法不同在于,后两个方法可以指定线程池参数。
  • 默认情况下CompletableFuture会使用公共的ForkJoinPool线程池,这个线程池默认创建的线程数是CPU的核数。如果所有CompletableFuture共享一个线程池,那么一旦有任务执行一些很慢的I/O操作,就会导致线程池中所有线程都阻塞在I/O操作上,从而造成线程饥饿,进而影响整个系统的性能。所以,强烈建议你要根据不同的业务类型创建不同的线程池,以避免互相干扰。

对于一个异步操作,你需要关注两个问题:一个是异步操作什么时候结束,另一个是如何获取异步操作的执行结果。因为CompletableFuture类实现了Future接口,所以这两个问题你都可以通过Future接口来解决。另外,CompletableFuture类还实现了CompletionStage接口。

3. 如何理解CompletionStage接口

任务是有时序关系的,比如有串行关系、并行关系、汇聚关系等。
在这里插入图片描述

3.1 描述串行关系

主要是thenApply、thenAccept、thenRun和thenCompose这四个系列的接口。

thenApply系列函数里参数fn的类型是接口Function<T, R>,这个接口里与CompletionStage相关的方法是 R apply(T t),这个方法既能接收参数也支持返回值,所以thenApply系列方法返回的是CompletionStage。

而thenAccept系列方法里参数consumer的类型是接口Consumer,这个接口里与CompletionStage相关的方法是 void accept(T t),这个方法虽然支持参数,但却不支持回值,所以thenAccept系列方法返回的是CompletionStage。

thenRun系列方法里action的参数是Runnable,所以action既不能接收参数也不支持返回值,所以thenRun系列方法返回的也是CompletionStage。

这些方法里面Async代表的是异步执行fn、consumer或者action。其中,需要你注意的是thenCompose系列方法,这个系列的方法会新创建出一个子流程,最终结果和thenApply系列是相同的。

CompletionStage<R> thenApply(fn);
CompletionStage<R> thenApplyAsync(fn);
CompletionStage<Void> thenAccept(consumer);
CompletionStage<Void> thenAcceptAsync(consumer);
CompletionStage<Void> thenRun(action);
CompletionStage<Void> thenRunAsync(action);
CompletionStage<R> thenCompose(fn);
CompletionStage<R> thenComposeAsync(fn);

通过下面的示例代码,你可以看一下thenApply()方法是如何使用的。首先通过supplyAsync()启动一个异步流程,之后是两个串行操作,整体看起来还是挺简单的。不过,虽然这是一个异步流程,但任务①②③却是串行执行的,②依赖①的执行结果,③依赖②的执行结果。

CompletableFuture<String> f0 = 
  CompletableFuture.supplyAsync(
    () -> "Hello World")      //1
  .thenApply(s -> s + " QQ")  //2
  .thenApply(String::toUpperCase);//3

System.out.println(f0.join());
//输出结果
HELLO WORLD QQ

3.2 描述AND汇聚关系

主要是thenCombine、thenAcceptBoth和runAfterBoth系列的接口。

CompletionStage<R> thenCombine(other, fn);
CompletionStage<R> thenCombineAsync(other, fn);
CompletionStage<Void> thenAcceptBoth(other, consumer);
CompletionStage<Void> thenAcceptBothAsync(other, consumer);
CompletionStage<Void> runAfterBoth(other, action);
CompletionStage<Void> runAfterBothAsync(other, action);

3.3 描述OR汇聚关系

主要是applyToEither、acceptEither和runAfterEither系列的接口.

CompletionStage applyToEither(other, fn);
CompletionStage applyToEitherAsync(other, fn);
CompletionStage acceptEither(other, consumer);
CompletionStage acceptEitherAsync(other, consumer);
CompletionStage runAfterEither(other, action);
CompletionStage runAfterEitherAsync(other, action);

展示了如何使用applyToEither()方法来描述一个OR汇聚关系。

CompletableFuture<String> f1 = 
  CompletableFuture.supplyAsync(()->{
    int t = getRandom(5, 10);
    sleep(t, TimeUnit.SECONDS);
    return String.valueOf(t);
});

CompletableFuture<String> f2 = 
  CompletableFuture.supplyAsync(()->{
    int t = getRandom(5, 10);
    sleep(t, TimeUnit.SECONDS);
    return String.valueOf(t);
});

CompletableFuture<String> f3 = 
  f1.applyToEither(f2,s -> s);

System.out.println(f3.join());

3.4 异常处理

上面我们提到的fn、consumer、action它们的核心方法都不允许抛出可检查异常,但是却无法限制它们抛出运行时异常。

CompletionStage接口给我们提供的方案非常简单,比try{}catch{}还要简单。

CompletionStage exceptionally(fn);
CompletionStage<R> whenComplete(consumer);
CompletionStage<R> whenCompleteAsync(consumer);
CompletionStage<R> handle(fn);
CompletionStage<R> handleAsync(fn);

下面的示例代码展示了如何使用exceptionally()方法来处理异常,exceptionally()的使用非常类似于try{}catch{}中的catch{},但是由于支持链式编程方式,所以相对更简单。既然有try{}catch{},那就一定还有try{}finally{},whenComplete()和handle()系列方法就类似于try{}finally{}中的finally{},无论是否发生异常都会执行whenComplete()中的回调函数consumer和handle()中的回调函数fn。whenComplete()和handle()的区别在于whenComplete()不支持返回结果,而handle()是支持返回结果的。

CompletableFuture<Integer> 
  f0 = CompletableFuture
    .supplyAsync(()->7/0))
    .thenApply(r->r*10)
    .exceptionally(e->0);
System.out.println(f0.join());

4. 总结

可以了解RxJava

5. 课后思考

创建采购订单的时候,需要校验一些规则,例如最大金额是和采购员级别相关的。有同学利用CompletableFuture实现了这个校验的功能,逻辑很简单,首先是从数据库中把相关规则查出来,然后执行规则校验。你觉得他的实现是否有问题呢?

//采购订单
PurchersOrder po;
CompletableFuture<Boolean> cf = 
  CompletableFuture.supplyAsync(()->{
    //在数据库中查询规则
    return findRuleByJdbc();
  }).thenApply(r -> {
    //规则校验
    return check(po, r);
});
Boolean isOk = cf.join();
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值