文章目录
引言
在高性能编程中,并发编程已经成为了极为重要的一部分。并发编程可以总结为三个核心问题:分工、同步和互斥。编写并发程序,首先要做的就是分工,所谓分工指的是如何高效地拆解任务并分配给线程。由于并发编程比串行编程更困难,也更容易出错,因此,我们就更需要借鉴一些前人优秀的,成熟的设计模式,使得我们的设计更加健壮,更加完美。
而Future模式,正是其中使用最为广泛,也是极为重要的一种设计模式。今天就跟少侠了解一手Future模式!
生活中的例子
场景1
小张喜欢没事泡泡茶,每次都是洗水壶–>洗茶壶–>洗茶杯–>烧开水–>拿茶叶–>泡茶,如下图,喝到茶大概得花上20分钟。
场景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模式的应用极其广泛,可以说这个模式已经成为了异步开发的基础设施。