【Java并发编程实战】(十七):Future和CompletableFuture的原理及实战——异步编程没有那么难

导读:本篇文章讲解 【Java并发编程实战】(十七):Future和CompletableFuture的原理及实战——异步编程没有那么难,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

引言

在高性能编程中,并发编程已经成为了极为重要的一部分。并发编程可以总结为三个核心问题:分工、同步和互斥。编写并发程序,首先要做的就是分工,所谓分工指的是如何高效地拆解任务并分配给线程。由于并发编程比串行编程更困难,也更容易出错,因此,我们就更需要借鉴一些前人优秀的,成熟的设计模式,使得我们的设计更加健壮,更加完美。
而Future模式,正是其中使用最为广泛,也是极为重要的一种设计模式。今天就跟少侠了解一手Future模式!

生活中的例子

场景1

小张喜欢没事泡泡茶,每次都是洗水壶–>洗茶壶–>洗茶杯–>烧开水–>拿茶叶–>泡茶,如下图,喝到茶大概得花上20分钟。
午饭时间到了,同学们都去吃饭了。小张下楼到肯德基,排队、点餐、进餐, 回公司,一共花了60分钟。

场景2

但是小王不这么干,对于烧水泡茶这个程序,她采取的方案是下图所示的这样:用两个线程T1和T2来完成烧水泡茶程序,T1负责洗水壶、烧开水、泡茶这三道工序,T2负责洗茶壶、洗茶杯、拿茶叶三道工序,其中T1在执行泡茶这道工序时需要等待T2完成拿茶叶的工序。对于T1的这个等待动作,你应该可以想出很多种办法,例如Thread.join()、CountDownLatch,甚至阻塞队列都可以解决,不过今天我们用Future特性来实现。
在这里插入图片描述

Java中的Future

如何获取Future

// 提交Runnable任务
Future submit(Runnable task);

这个方法的参数是一个Runnable接口,Runnable接口的run()方法是没有返回值的,所以 submit(Runnable task) 这个方法返回的Future仅可以用来断言任务已经结束了,类似于Thread.join()。

// 提交Callable任务
 Future submit(Callable task);

这个方法的参数是一个Callable接口,它只有一个call()方法,并且这个方法是有返回值的,所以这个方法返回的Future对象可以通过调用其get()方法来获取任务的执行结果。

// 提交Runnable任务及结果引用  
 Future submit(Runnable task, T result);

这个方法很有意思,假设这个方法返回的Future对象是future,future.get()的返回值就是传给submit()方法的参数result。这个方法该怎么用呢?下面这段示例代码展示了它的经典用法。需要你注意的是Runnable接口的实现类Task声明了一个有参构造函数 Task(Result r) ,创建Task对象的时候传入了result对象,这样就能在类Task的run()方法中对result进行各种操作了。result相当于主线程和子线程之间的桥梁,通过它主子线程可以共享数据。

在这里插入图片描述

Future的主要方法及使用

获取到Future之后,我们怎么来进行使用呢,Java中提供了如下几个核心方法:

	// 取消任务
	boolean cancel(boolean mayInterruptIfRunning);
	// 判断任务是否已取消  
	boolean isCancelled();
	// 判断任务是否已结束
	boolean isDone();
	// 获得任务执行结果
	get();
	// 获得任务执行结果,支持超时
	get(long timeout, TimeUnit unit);

那么如何灵活的使用这几个方法呢?下面的示例代码就是用这一节提到的Future特性来实现的。首先,我们创建了两个Future——task1和task2,task1完成洗水壶、烧开水、泡茶的任务,task2完成洗茶壶、洗茶杯、拿茶叶的任务;这里需要注意的是task1这个任务在执行泡茶任务前,需要等待task2把茶叶拿来,所以task1内部需要引用task2,并在执行泡茶之前,调用task2的get()方法实现等待。
在这里插入图片描述
一次执行结果:

thread13--->洗茶壶
thread14--->洗水壶
thread13--->洗茶杯
thread14--->烧开水
thread13--->拿茶叶
thread14--->泡茶
泡茶吧!来一杯大红袍!
elapsed: 16

Future的核心源码

那么Future又是如何实现异步操作的呢,我们结合源码来看一下。
在这里插入图片描述
由于Future是接口,这里我们主要看它的实现类FutureTask的实现。关键的部分在下面,FutureTask作为一个线程单独执行时,会将结果保存到Object类型的变量outcome中,并设置任务的状态,下面是FutureTask的run()方法:
在这里插入图片描述
从FutureTask中获得结果的实现如下:
在这里插入图片描述

Future模式的高阶版本—— CompletableFuture

Future模式虽然好用,但也有一个问题,那就是将任务提交给线程后,调用线程并不知道这个任务什么时候执行完,如果执行调用get()方法或者isDone()方法判断,可能会进行不必要的等待,那么系统的吞吐量很难提高。
为了解决这个问题,JDK对Future模式又进行了加强,创建了一个CompletableFuture,它可以理解为Future模式的升级版本,它最大的作用是提供了一个回调机制,可以在任务完成后,自动回调一些后续的处理,这样,整个程序可以把“结果等待”完全给移除了。

如何获取CompletableFuture

public static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

四个静态方法用来为一段异步执行的代码创建CompletableFuture对象,方法的参数类型都是函数式接口,所以可以使用lambda表达式实现异步任务

  • runAsync方法:它以Runnable函数式接口类型为参数,所以CompletableFuture的计算结果为空。

  • supplyAsync方法以Supplier函数式接口类型为参数,CompletableFuture的计算结果类型为U。

说明:Async结尾的方法都是可以异步执行的,如果指定了线程池,会在指定的线程池中执行,如果没有指定,默认会在ForkJoinPool.commonPool()中执行。

CompletableFuture的主要方法及使用

关于CompletableFuture,Java中提供了如下几个核心方法:
1 变换结果
由于回调风格的实现,我们不必因为等待一个计算完成而阻塞着调用线程,而是告诉CompletableFuture当计算完成的时候请执行某个Function。还可以串联起来。
这些方法的输入是上一个阶段计算后的结果,返回值是经过转化后结果:

public <U> CompletionStage<U> thenApply(Function<? super T,? extends U> fn);
public <U> CompletionStage<U> thenApplyAsync(Function<? super T,? extends U> fn);
public <U> CompletionStage<U> thenApplyAsync(Function<? super T,? extends U> fn,Executor executor);

2 消费结果
这些方法只是针对结果进行消费,入参是Consumer,没有返回值:

public CompletionStage<Void> thenAccept(Consumer<? super T> action);
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action);
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action,Executor executor);

3 计算结果完成时的处理
当CompletableFuture的计算结果完成,或者抛出异常的时候,可以执行特定的Action。主要是下面的方法:

public CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action)
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action)
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor)
public CompletableFuture<T> exceptionally(Function<Throwable,? extends T> fn)

4 结合两个CompletionStage的结果,进行转化后返回
需要上一阶段的返回值,并且other代表的CompletionStage也要返回值之后,把这两个返回值,进行转换后返回指定类型的值。

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

为了和大家一起体会CompletableFuture异步编程的优势,这里我们用CompletableFuture重新实现前面曾提及的烧水泡茶程序。首先还是需要先完成分工方案,在下面的程序中,我们分了3个任务:任务1负责洗水壶、烧开水,任务2负责洗茶壶、洗茶杯和拿茶叶,任务3负责泡茶。其中任务3要等待任务1和任务2都完成后才能开始。这个分工如下图所示。
在这里插入图片描述
下面是具体代码实现,你先略过runAsync()、supplyAsync()、thenCombine()这些不太熟悉的方法,从大局上看,你会发现:

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

在这里插入图片描述

小结

今天我们主要介绍Future模式,我们从一个最简单的Future模式开始,逐步深入,先后介绍了JDK内部的Future模式实现,以及对Future模式的进化版本CompletableFuture做了简单的介绍。对
于多线程开发而言,Future模式的应用极其广泛,可以说这个模式已经成为了异步开发的基础设施。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/13453.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!