Future模式与异步回调模式 您所在的位置:网站首页 java中的future类 Future模式与异步回调模式

Future模式与异步回调模式

#Future模式与异步回调模式| 来源: 网络整理| 查看: 265

写再前面

Future模式与异步回调模式二者十分相似又存在区别,所以将两个和在一起进行总结。 ​

Future模式 什么是Future模式 长篇大论

用生活中的例子来打个比喻,就像叫外卖。比如在午休之前我们可以提前叫外卖,只需要点好食物,下个单。然后我们可以继续工作。到了中午下班的时候外卖也就到了,然后就可以吃个午餐,再美滋滋的睡个午觉。而如果你在下班的时候才叫外卖,那就只能坐在那里干等着外卖小哥,最后拿到外卖吃完午饭,午休时间也差不多结束了。 ​

Future 模式是高并发设计与开发过程中常见的设计模式,它的核心思想是异步调用。对于 Future 模式来说,它不是立即返回你需要的数据,但是它会返回一个契约(或者说异步任务),将来你可以凭借这个契约(或异步任务)去获取你需要的结果。 ​

在进行传统的 RPC(远程调用)时,同步调用 RPC 是一段耗时的过程。当客户端发出 RPC请求,服务端完成请求处理需要很长的一段时间才会返回,这个过程中客户端一直在等待,直到 数据返回随后再进行其他任务的处理。现有一个 Client 同步对三个 Server 分别进行一次 RPC 调 用。 image.png 假设一次远程调用的时间为 500ms,则一个 Client 同步对三个 Server 分别进行一次 RPC 调 用的总时间,需要耗费 1500ms。如果节省这个总时间呢,可以使用 Future 模式对其进行改造,将同步的 RPC 调用改为异步并发的 RPC 调用,一个 Client 异步并发对三个 Server 分别进行一次 RPC 调用 image.png

假设一次远程调用的时间为 500ms,则一个 Client 异步并发对三个 Server 分别进行一次 RPC调用的总时间,还只要耗费 500ms。使用 Future 模式异步并发地进行 RPC 调用,客户端在得到一 个 RPC 的返回结果前,并不急于获取该结果,而是充分利用等待时间去执行其他的耗时操作(如其他 RPC 调用)这就是 Future 模式的核心所在。 Future 模式的核心思想是异步调用,有点类似于异步的 Ajax 请求。当调用某个耗时方法时, 可以不急于立刻获取结果,可以让被调用者立刻返回一个契约(或异步任务), 并且将耗时的方法放到另外线程执行,后续凭契约再去获取异步执行的结果。 在具体的实现上,Future 模式和异步回调模式既有区别,又有联系。Java 的 Future 模式实现,没有实现异步回调模式,仍然需要主动去获取耗时任务的结果;而 Java 8 中的 CompletableFuture 组件,实现了异步回调模式。 ​

一句话总结

使用Future模式,获取数据的时候无法立即得到需要的数据。而是先拿到一个契约,你可以再将来需要的时候再用这个契约去获取需要的数据,这个契约就好比叫外卖的例子里的外卖订单。 Futute模式核心在于去除了主调用函数的等待时间,并使得原本需要等待的时间可以充分利用来处理其他业务逻辑,充分的利用了系统资源。 ​

简单版本的实现

我通过上面的点外卖的故事来实现一个简单的Future模式,这样更利于大家领会其中的奥妙。 ​

首先是FutureData,它是只是一个包装类,创建它不需要耗时。在工作线程准备好数据之后可以使用setData方法将数据传入。而客户端线程只需要在需要的时候调用getData方法即可,如果这个时候数据还没有准备好,那么getData方法就会等待,如果已经准备好了就好直接返回。

/*用于存储返回结果*/ public class FutureData { /*标志位用于判断数据是否已经存储完成*/ private boolean mIsReady=false; private T mData; public synchronized void setData(T data){ mIsReady=true; mData=data; /*唤醒操作*/ notifyAll(); } public synchronized T getData(){ /*wait: * 让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。 * “直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”, * 当前线程被唤醒(进入“就绪状态”) * */ if (!mIsReady){ try { /*如果没有则数据准备好则进入等待状态,等待唤醒*/ wait(); } catch (InterruptedException e) { e.printStackTrace(); } } return mData; } }

接着是服务端,客户端在向服务端请求数据的时候服务端不会实际去加载数据,它只是创建一个FutureData,然后创建子线程去加载,而它只需要直接返回FutureData就可以了。 ​

public class Server { public FutureData getData(){ final FutureData data = new FutureData(); new Thread(new Runnable() { @Override public void run() { try { /*睡眠1000毫秒,模拟RPC远程调用的执行时间*/ Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } /*向FutureData中加入数据*/ data.setData("炸鸡到了!!"); } }).start(); return data; } }

客户端代码如下。

public class Main { public static void main(String[] args) throws InterruptedException { Server server = new Server(); /*返回一个Future,后续通过Future对象去取实际的内容*/ System.out.println("点个炸鸡吃吃!!"); FutureData future = server.getData(); System.out.println("炸鸡没到,先处理其他事情。。。"); /*睡眠1000秒模拟处理其他事情*/ Thread.sleep(1000); System.out.println("其他事情处理好了!!去看看炸鸡到没!!"); /*获取Future中的数据,如果准备好了,则直接获取,如果没有则阻塞*/ String data = future.getData(); System.out.println("炸鸡到了!!"); } }

从上述的案例我们可以看到节省的时间为等待外卖的过程中我们又同时处理了其他的事情。 JDK中的实现版本肯定不会像我这般简陋,下面我们来学习学习JDK中是如何实现的。 ​

JDK中的实现 Runable接口

在多线程的编程中我们经常使用到Runable接口,Runable接口有如下众多优点。 ​

**灵活:**Runnable可以继承其他类实现对Runnable实现类的增强,避免了Thread类由于继承Thread类而无法继承其他类的问题。 **共享资源: **Runnable接口的run()方法可以被多个线程共享,适用于多个进程处理一种资源的问题。 ​

**缺点: **但是Runable接口有一个巨大的缺点,就是他执行完任务不会返回结果,这在很多场景下是不适用的。 ​

所以为了执行异步执行的结果问题,Java语言在1.5 版本之后提供了一种的新的多线程创建方法: 通过 Callable 接口和 FutureTask 类相结合创建线程。 ​

Callable接口

Callable 接口位于 java.util.concurrent 包中,翻开 Java 源代码,Callable 的代码如下:

@FunctionalInterface public interface Callable { /** * Computes a result, or throws an exception if unable to do so. * * @return computed result * @throws Exception if unable to compute a result */ V call() throws Exception; }

Callable 接口是一个泛型接口,也是一个“函数式接口”。其唯一的抽象方法 call()有返回 值,返回值的类型为 Callable 接口的泛型形参类型;call 抽象方法还有一个 Exception 的异常声明,容许方法的实现版本内部的异常直接抛出,并且可以不予捕获(子父类之间异常处理的关系)。 ​

子类声明异常不能超出父类的范围 [1]父类没有声明异常,子类也不能 [2]不可抛出原有方法抛出异常类的父类或上层类 [3]抛出的异常类型的数目不可以比原有的方法抛出的还多(不是指个数) [4]如果程序有多个catch,应该子类异常在前,父类异常在后。

Callable 接口类似于 Runnable。不同的是,Runnable 的唯一抽象方法 run()没有返回值,也 没有受检异常的异常声明。比较而言,Callable 接口的 call()有返回值有返回值,并且申明了受 检异常,其功能更强大一些。 ​

问题:Callable 实例能否和 Runnable 实例一样,作为 Thread 线程实例的 target 来使用吗?答 案是不行:Thread 的 target 属性的类型为 Runnable,而 Callable 接口与Runnable 接口之间没有任 何的继承关系,并且二者唯一方法在的名字上也不同。显而易见,Callable 接口实例没有办法作为 Thread 线程实例的 target 来使用。既然如此,那么该如何使用 Callable 接口去创建线程呢?一个 重要的在 Callable 接口与 Thread 线程之间起到搭桥作用的接口,马上就要登场了。 ​

RunableFuture接口

这个重要中间搭桥接口,就是 RunnableFuture 接口,该接口与 Runnable 接口、Thread 类紧密 相关的。与 Callable 接口一样,RunnableFuture 接口也是位于 java.util.concurrent 包,使用的时候需要的 import 导入。 RunnableFuture 是如何在 Callable 与 Thread 中间实现搭桥功能的呢?RunnableFuture 接口实现了两个目标:一是可以作为 Thread 线程实例的 target 实例;二是可以获取异步执行的结果。它 是如何做到一箭双雕的呢?请看 RunnableFuture 的接口的代码:

public interface RunnableFuture extends Runnable, Future { /** * Sets this Future to the result of its computation * unless it has been cancelled. */ void run(); }

通过源码可以看出,RunnableFuture 继承 Runnable 接口,从而保证了其实例可以作为 Thread线程实例的 target 目标;同时,RunnableFuture 通过继承 Future 接口,从而保证了通过它可以获 取未来的异步执行结果。 在这里,一个新的、从来没有介绍过的、又非常重要的 Future 接口,马上登场。 ​

Future接口

需要实现的基本功能: ​

(1)能够取消异步执行中的任务。 (2)判断异步任务是否执行完成。 (3)获取异步任务完成后的执行结果。 ​

源码:

public interface Future { boolean cancel(boolean mayInterruptIfRunning); boolean isCancelled(); boolean isDone(); V get() throws InterruptedException, ExecutionException; V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; } V get():获取异步任务执行的结果。注意,这个方法的调用是阻塞性的。如果 异步任务没有执行完成,异步结果获取线程(调用线程)会一直被阻塞,一直阻塞到到异步任务执行完成,其异步结果返回给调用线程。V get(Long timeout , TimeUnit unit) :设置时限,(调用线程)阻塞性的获取异 步任务执行的结果。该方法的调用也是阻塞性的,但是结果获取线程(调用线程)会有一 个阻塞时长限制,不会无限制的阻塞和等待,如果其阻塞时间超过设定的 timeout 时间,该方法将抛出异常,调用线程可捕获此异常。boolean isDone():获取异步任务的执行状态。如果任务执行结束,返回 true。boolean isCancelled():获取异步任务的取消状态。如果任务完成前被取消,则 返回 true。boolean cancel(boolean mayInterruptRunning):取消异步任务的执行。

总体来说,Future 是一个对异步任务进行交互、操作的接口。但是 Future 仅仅是一个接口, 通过它没有办法直接完成对异步任务的操作,JDK 提供了一个默认的实现了——FutureTask 类。 ​

FutureTask类

FutureTask 类是 Future 接口的实现类,提供对异步任务的操作的具体实现。但是,FutureTask 类不仅仅实现了 Future 接口,而且实现了 Runnable 接口,或者更加准确地说,FutureTask 类实现 了 RunnableFuture 接口。 前面讲到 RunnableFuture 接口很关键,既可以作为 Thread 线程实例的 target 目标,也可以获 取并发任务执行的结果,是 Thread 与 Callable 之间一个非常重要的搭桥角色。但是,RunnableFuture 只是一个接口,无法直接创建对象,如果需要创建对象,就需用到它的实现类——FutureTask 类。 所以说,FutureTask 类才是真正的、最终的 Thread 与 Callable 之间的搭桥类。 从 FutureTask 类的 UML 关系图可以看到:FutureTask 实现了 RunnableFuture 接口,而 RunnableFuture 接口继承了 Runnable 接口和 Future 接口,所以,FutureTask 既能当做一个 Runnable 类型的 target 执行目标直接被 Thread 执行,也能作为 Future 异步任务来获取 Callable 的计算结 果。 FutureTask 如何完成多线程的并发执行、任务结果的异步获取的呢?FutureTask 内部有一个 Callable 类型的成员——callable 实例属性,具体如下:

private Callable callable;

callable 实例属性用来保存并发执行的 Callable类型的任务,并且,callable 实例属性需要 在 FutureTask 实例构造时进行初始化。FutureTask 类实现了 Runnable 接口,在其 run()方法的实现 版本中,会执行 callable 成员的 call()方法。 ​

此外,FutureTask 内部还有另一个非常重要的 Object 类型的成员——outcome 实例属性:

private Object outcome;

**FutureTask的outcome实例属性用于保存callable成员call()方法的异步执行结果。**在FutureTask 类 run()方法完成 callable 成员的 call()方法的执行之后,其结果将被保存在 outcome 实例属性中, 供 FutureTask 类的 get()方法去获取。 ​

总体继承关系图 ​

image.png

使用Callable和FutureTask创建线程的具体步骤

仍然以点外卖的的背景来实现这个案例,等待外卖派送的过程就相当于RPC远程调用等待的过程!! ​

基本步骤: ​

(1)创建一个 Callable 接口的实现类,并实现其 call()方法,编写好异步执行的具体逻辑, 并且可以有返回值。 (2)使用 Callable 实现类的实例,构造一个 FutureTask 实例。 (3)使用 FutureTask 实例,作为 Thread 构造器的 target 入参,构造新的 Thread 线程实例; (4)调用 Thread 实例的 start 方法启动新线程,启动新线程的 run()方法并发执行。其内部的 执行过程为:启动 Thread 实例的 run()方法并发执行后,会执行 FutureTask 实例的 run()方法,最 终会并发执 Callable 实现类的 call()方法。 (5)调用 FutureTask 对象的 get()方法,阻塞性的获得并发线程的执行结果。

public class CallableUse { public static final int MAX_TURN = 5; public static final int COMPUTE_TIMES = 100000000; public static void main(String[] args) throws InterruptedException, ExecutionException { /*实现一个Callable接口*/ Callable callableImp = new Callable() { //编写好异步执行的具体逻辑,并且可以有返回值 @Override public String call() throws Exception { System.out.println("订单制作派送中。。。"); Thread.sleep(1050); return "外卖到了!!"; } }; /*创建一个futureTask类*/ FutureTask futureTask = new FutureTask(callableImp); /*创建一个线程进行执行*/ System.out.println("肚子饿了,点个外卖!!"); new Thread(futureTask,"returnableThread").start(); /*主线程模拟干其他事情*/ System.out.println("外卖还没到,干点其他事情"); Thread.sleep(1000); System.out.println("任务干完了,看看外卖到没!!"); /*这是一个阻塞操作*/ String s = futureTask.get(); System.out.println(s); } }

执行结果: ​

肚子饿了,点个外卖!! 外卖还没到,干点其他事情 订单制作派送中。。。 任务干完了,看看外卖到没!! 外卖到了!!

流程分析: ​

在这个例子中有两个线程:一个是执行 main 方法的主线程,名字叫做“main”;另一个是 “main”线程通过 thread.start 方法启动的业务线程,叫做“returnableThread”线程。该线程是一 个包含了 FutureTask 任务作为 target 的 Thread 线程。 “main”线程通过 thread.start()启动“returnableThread”线程之后,“main”线程会继续自己的事情。“returnableThread”线程开始并发执行。 “returnableThread”线程首先开始执行的,是 thread.run 方法,然后在其中会执行到其 target (futureTask 任务)的 run 方法;接着在这个 futureTask.run 方法中,会执行 futureTask 的 callable 成员的 call 方法,在这里的 callable 成员(ReturnableTask 实例)是通过 FutureTask 构造器在初始 化时传递进来的、自定义的 Callable 实现类的实例。 “main”线程和“returnableThread”线程的执行流程,大致如图 1-9 所示。 image.png

FutureTask 的 Callable 成员的 call()方法执行完成后,会将结果保存在 FutureTask 内部的 outcome 实例属性中。以上演示实例的 Callable 实现类中,这里 call()方法中业务逻辑的返回结果,是 "外卖到了!!"这句话。 ​

"外卖到了!!"这句话被返回之后,作为结果将被保存在 FutureTask 内部的 outcome 实例属性中,至此, 异步的“returnableThread”线程执行完毕。在“main”线程处理完自己的事情(以上实例中是一 个消磨时间的循环)后,通过 futureTask 的 get 实例方法获取异步执行的结果。这里有两种情况: (1)futureTask 的结果 outcome 不为空,callable.call()执行完成;在这种情况下,futureTast.get 会直接取回 outcome 结果,返回给“main”线程(结果获取线程)。 (2)futureTask 的结果 outcome 为空,callable.call()还没有执行完。 在这种情况下,“main”线程作为结果获取线程会被阻塞住,一直被阻塞到 callable.call()执行 完成。当执行完后,最终结果保存到 outcome 中,futureTask 会唤醒的“main”线程,去提取callable.call()执行结果。

异步回调模式 FutureTask的缺点

通过 FutureTask 的 get 方法获取异步结果时,主线程也会被阻塞的。是异步阻塞模式。异步阻塞的效率往往是比较低的,被阻塞的主线程,不能干任何事情,唯一能干的,就是在 傻傻等待。原生 Java API,除了阻塞模式的获取结果外,并没有实现非阻塞的异步结果获取方法。 如果需要用到获取异步的结果,得引入一些额外的框架,这里首先介绍谷歌的 Guava 框架。 ​

异步调用与主动调用

主动调用: ​

主动调用是一种阻塞式调用,它是一种单向调用,“调用方”要等待“被调用方”执行完毕 才返回。如果“被调用方”的执行的时间很长,那么“调用方”线程需要阻塞很长一段时间。就像FutureTask的get方法。 ​

异步调用: ​

在回调模式中负责执行回调方法的具体线程已经不再是调用方线程(而是变成了异步的被调用方线程(如烧水线程)。 Java 中回调模式的标准实现类为 CompletableFuture,由于该类出现的时间比较晚,所以很 多的著名的中间件如 Guava、Netty 等都提供了自己的异步回调模式 API 供开发者们使用。开发 者还可以使用 RxJava 响应式编程组件进行异步回调的开发。 ​

Guava 的异步回调模式

Guava 是 Google 提供的 Java 扩展包,它提供了一种异步回调的解决方案。Guava 中与异步 回调相关的源码,处于 com.google.common.util.concurrent 包中。包中的很多类都是对 java.util.concurrent 能力扩展和能力增强。比如,Guava 的异步任务接口 ListenableFuture 扩展了 Java 的 Future 接口,实现了异步回调的的能力。 ​

FutureCallback

总体来说,Guava 的主要增强了 Java 而不是另起炉灶。为了实现异步回调方式获取异步线程 的结果,Guava 做了以下的增强:

引入得了一个新的接口 ListenableFuture,继承了 Java 的 Future 接口,使得 Java 的 Future 异步任务,在 Guava 中能被监控和非阻塞获取异步结果。引入了一个新的接口 FutureCallback,这是一个独立的新接口。该接口的目的是在异步任务执行完成后,根据异步结果完成不同的回调处理,并且可以处理异步结果。

FutureCallback 是一个新增的接口,用来填写异步任务执行完后的监听逻辑。FutureCallback拥有两个回调方法:

onSuccess 方法,在异步任务执行成功后被回调;调用时,异步任务的执行结果,作为onSuccess 方法的参数被传入。onFailure 方法,在异步任务执行过程中,抛出异常时被回调;调用时,异步任务所抛出的 异常,作为onFailure 方法的参数,被传入。 @GwtCompatible public interface FutureCallback { /** * Invoked with the result of the {@code Future} computation when it is successful. */ void onSuccess(@Nullable V result); /** * Invoked when a {@code Future} computation fails or is canceled. * *

If the future's {@link Future#get() get} method throws an {@link ExecutionException}, then * the cause is passed to this method. Any other thrown object is passed unaltered. */ void onFailure(Throwable t); } 详解 ListenableFuture

image.png

看 ListenableFuture 接口名称,知道它与 Java 中 Future 接口的亲戚关系。没错,Guava 的ListenableFuture 接口是对 Java 的 Future 接口的扩展,可以理解为异步任务实例。源码如下:

@GwtCompatible public interface ListenableFuture extends Future { void addListener(Runnable listener, Executor executor); }

ListenableFuture 仅仅增加了一个 addListener 方法。它的作用就是将前一小节的 FutureCallback善后回调逻辑,封装成一个内部的 Runnable 异步回调任务,在 Callable 异步任务完成后,回调 FutureCallback 善后逻辑。 ​

注意,此 addListener 方法只在 Guava 内部使用,如果对它感兴趣,可以查看 Guava 源码。在实际编程中,addListener 不会使用到。 在实际编程中,如何将 FutureCallback 回调逻辑绑定到异步的 ListenableFuture 任务呢?可以 使用 Guava 的 Futures 工具类,它有一个 addCallback 静态方法,可以将 FutureCallback 的回调实例绑定到 ListenableFuture 异步任务。下面是一个简单的绑定实例: ​

Futures.addCallback(listenableFuture, new FutureCallback() { public void onSuccess(Boolean r) { // listenableFuture 内部的 Callable 成功时候的回调此方法 } public void onFailure(Throwable t) { // listenableFuture 内部的 Callable 异常时候的回调此方法 } }); ListenableFuture 异步任务

如果要获取 Guava 的 ListenableFuture 异步任务实例,主要是通过向线程池(ThreadPool)提 交 Callable 任务的方式获取。不过,这里所说的线程池,不是 Java 的线程池,而是经过 Guava 自己的定制过的 Guava 线程池。 Guava 线程池是对 Java 线程池的一种装饰。创建 Guava 线程池的方法如下:

//java 线程池 ExecutorService jPool = Executors.newFixedThreadPool(10); // Guava 线程池 ListeningExecutorService gPool = MoreExecutors.listeningDecorator(jPool);

首先创建 Java 线程池,然后以其作为 Guava 线程池的参数,再构造一个 Guava 线程池。有 了 Guava 的线程池之后,就可以通过 submit 方法来提交任务了;任务提交之后的返回结果,就是 我们所要的 ListenableFuture 异步任务实例了。 简单来说:获取异步任务实例的方式,通过向线程池提交 Callable 业务逻辑来实现。代码如 下:

//submit 方法来提交任务,返回异步任务实例 ListenableFuture hFuture = gPool.submit(hJob); //绑定回调实例 Futures.addCallback(listenableFuture, new FutureCallback() { //有两种实现回调的方法 });

取到了 ListenableFuture 实例后,通过 Futures.addCallback 方法,将 FutureCallback 回调逻辑的实例,绑定到 ListenableFuture 异步任务实例,实现异步执行完成后的回调。 总结一下,Guava 异步回调的流程如下: **第一步:**实现 Java 的 Callable 接口,创建的异步执行逻辑。还有一种情况,如果不需要返回 值,异步执行逻辑也可以实现 Runnable 接口。 **第二步:**创建 Guava 线程池。 **第三步:**将第一步创建的 Callable/Runnable 异步执行逻辑的实例,submit 提交到 Guava 线程 池,从而获取 ListenableFuture 异步任务实例。 **第四步:**创建 FutureCallback 回调实例,通过 Futures.addCallback,将回调实例绑定到ListenableFuture 异步任务上。 完成以上四步,当 Callable/Runnable 异步执行逻辑完成后,就会回调异步回调实例FutureCallback 实例的回调方法 onSuccess/onFailure。 ​

案例实现

这里需要将前面的外卖案例改造一下。改造成以下背景: 你女朋友饿了需要你给他点个外卖,你只需要给他点了就好了,因为你已经把他的电话号码地址等等都写在了订单上,商家在把外卖制作完成之后知道怎么把外卖送到你女朋友的手上你不需要再去关心任何事情。 ​

package com.Test.Future.GuavaFutureDemo; import com.google.common.util.concurrent.*; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @ClassName: GuavaFutureDemo * @Author: 86151 * @Date: 2021/11/5 16:15 * @Description: TODO */ public class GuavaFutureDemo { static class callableImp implements Callable{ @Override public String call() throws Exception { System.out.println("开始制作外卖!!"); /*模拟外卖制作消耗的时间*/ Thread.sleep(1000); return "宫保鸡丁"; } } static void eatFood(){ System.out.println("女朋友收到外卖开始吃外卖!!"); } public static void main(String[] args) { /*创建一个callable实现类*/ callableImp callableImp = new callableImp(); /*创建一个线程池*/ ExecutorService executorService = Executors.newFixedThreadPool(10); /*包装Java线程池,构造guava线程池*/ ListeningExecutorService gPool = MoreExecutors.listeningDecorator(executorService); /*设置钩子函数*/ FutureCallback hook = new FutureCallback() { /*在里面执行数据返回的业务逻辑*/ @Override public void onSuccess(String s) { System.out.println(s+"到了..."); /*执行的具体业务逻辑这里省略*/ /*这里可以理解为将外卖送到后你女朋友开始吃外卖*/ eatFood(); /*相当于RPC远程调用返回数据后执行什么操作*/ } /*执行错误的执行逻辑*/ @Override public void onFailure(Throwable throwable) { System.out.println("外卖丢了。。。"); System.out.println("执行退款赔偿流程。。"); } }; /*开始点外卖*/ System.out.println("开始点外卖!!"); ListenableFuture hotFuture = gPool.submit(callableImp); /*设置回调钩子*/ Futures.addCallback(hotFuture,hook); } } Guava异步回调与Java异步调用的区别

(1)FutureTask 是主动调用的模式,“调用线程”主动获得异步结果;在获取异步结果时处 于阻塞状态,并且会一直阻塞,直到拿到异步线程的结果。 (2)Guava 是异步回调模式,“调用线程”不会主动去获得异步结果,而是准备好回调函数, 并设置好回调钩子;执行回调函数的并不是“调用线程”自身,回调函数的执行者,是“被调用 线程”;“调用线程”在执行完自己的业务逻辑后,就已经结束了;当回调函数被执行时,“调 用线程”可以已经结束很久了。相当于主函数先编写好了回调结果的处理逻辑让被调用线程去执行。 ​

Netty异步回调模式

Netty 官方文档说明——Netty 的网络操作都是异步的。Netty 源码中,大量使用了异步回调 处理模式。在 Netty 的业务开发层面,处于 Netty 应用的 Handler 处理器中的业务处理代码,也都是异步执行的。所以,了解 Netty 的异步回调,无论是 Netty 应用开始还是源码级开发,都是十分重要的。 Netty 和 Guava 一样,实现了自己的异步回调体系:Netty 继承和扩展了 JDK Future 系列异步 回调的 API,定义了自身的 Future 系列接口和类,实现异步任务的监控、异步执行结果的获取。 **总体来说, Netty 对 Java Future 异步任务的 扩展如下: ** 继承 Java 的 Future 接口得到一个新的属于 Netty 自己的 Future 异步任务接口;该接口对原有的接口进行了增强,使得 Netty 异步任务,能够非阻塞的处理回调结果;注意,Netty 没有修改 Future的名称,只是调整了所在的包名,Netty 的 Future 类的包名和 Java 的 Future 接口的包不同。 引入了一个新接口——GenericFutureListener,用于表示异步执行完成的监听器。这个接口和Guava 的 FutureCallbak 回调接口不同。Netty 使用了监听器的模式,异步任务的执行完成后的回 调逻辑,抽象成了 Listener 监听器接口。可以将 Netty 的 GenericFutureListener 监听器接口,加入Netty 异步任务 Future 中,实现对异步任务执行状态的事件监听。 总体来说,在异步非阻塞回调的设计思路上,Netty 和 Guava 的思路是一致的。对应关系为: (**1)Netty 的 Future 接口,可以对应到 Guava 的 ListenableFuture 接口; ** (2)Netty 的 GenericFutureListener 接口,可以对应到 Guava 的 FutrueCallback 接口。 ​

GenericFutureListener 接口详解

前面提到,和 Guava 的 FutrueCallback 一样,Netty 新增了一个接口,来封装异步非阻塞回调 的逻辑——它就是 GenericFutureListener 接口。 GenericFutureListener 位于 io.netty.util.concurrent 包中,源码如下

public interface GenericFutureListener extends EventListener { //监听器的回调方法 void operationComplete(F var1) throws Exception; }

GenericFutureListener 拥有一个回调方法:operationComplete,表示异步任务操作完成。在Future 异步任务执行完成后,将回调此方法。大多数情况下,Netty 的异步回调的代码,编写在 GenericFutureListener 接口的实现类中的 operationComplete 方法中。 说明下,GenericFutureListener 的父接口 EventListener,是一个空接口,没有任何的抽象方法, 是一个仅仅具有标识作用的接口。 ​

NettyFuture接口详解

Netty 也对 Java 的 Future 接口的扩展,并且名称没有变,还是叫做 Future 接口,实现在 io.netty.util.concurrent 包中。 和 Guava 的 ListenableFuture 一样,Netty 的 Future 接口扩展了一系列的方法,对执行的过程 的进行监控,对异步回调完成事件进行 Listen 监听,并且回调。Netty 的 Future 的源码如下:

public interface Future extends java.util.concurrent.Future { boolean isSuccess(); // 判断异步执行是否成功 boolean isCancellable(); // 判断异步执行是否取消 Throwable cause();//获取异步任务异常的原因 //增加异步任务执行完成 Listener 监听器 Future addListener(GenericFutureListener listener); //移除异步任务执行完成 Listener 监听器 Future removeListener(GenericFutureListener listener); //... }

Netty 的 Future 接口一般不会直接使用,使用过程中会使用器子接口。Netty 有一系列的子接 口,代表不同类型的异步任务,如 ChannelFuture 接口。 ChannelFuture 子接口表示 Channel 通道 I/O 操作的异步任务;如果在 Channel 的异步 I/O 操作完成后,需要执行回调操作,就需要使用到 ChannelFuture 接口。 ​

CompleteFuture异步回调

很多语言(如 JavaScript)提供了异步回调,一些 Java 中间件(如 Netty、Guava)也提供了 异步回调 API,为开发者带来更好的异步编程工具。Java8 提供一个新的、具备异步回调能力的工具类——CompletableFuture,该类实现了 Future 接口,并提供了异步回调的能力,还具备函数式 编程的能力。 ​

CompleteableFuture详解 继承关系图

Future 接口大家已经非常熟悉了,接下来介绍一下 CompletionStage 接口。CompletionStage 代 表异步计算过程中的某一个阶段,一个阶段完成以后可能会进入另外一个阶段。一个阶段可以理解为一个子任务,每一个子任务会包装一个 Java 函数式接口实例,表示该子任务所要执行的动作。 ​

image.png

CompletionStage接口

顾名思义,Stage 是阶段的意思。CompletionStage 它代表了某个同步或者异步计算的一个阶 段,或者是一系列异步任务中的一个子任务(或者阶段性任务)。 每个 CompletionStage 子任务所包装的可以是一个 Function、Consumer 或者 Runnable 函数式 接口实例。这三个常用的函数式接口的特点为: (1)Function Function 接口的唯一方法点是:有输入、有输出。包装了 Funtion 实例的 CompletionStage 子任务需要一个输入参数,并会产生一个输出结果到下一步。 (2)Runnable Runnable 接口的唯一方法点是:无输入、无输出。包装了 Runnable 实例的 CompletionStage 子任务既不需要任何输入参数,也不会产生任何输出。 (3)Consumer Consumer 接口的唯一方法点是:有输入、无输出。包装了 Consumer 实例的 CompletionStage子任务需要一个输入参数,但不会产生任何输出。 多个 CompletionStage 构成了一条任务流水线,一个环节执行完成了将结果可以移交给下一个环节(子任务)。多CompletionStage 子任务之间可以使用链式调用,下面是一个简单的例子:

oneStage.thenApply(x -> square(x)) .thenAccept(y -> System.out.println(y)) .thenRun(() -> System.out.println())

对以上例子中的 CompletionStage 子任务说明如下: (1)oneStage 是一个 CompletionStage 子任务,这是一个前提。 (2)“x -> square(x)” 是一个 Function 类型的 Lamda 表达式,被 thenApply 方法包装成了一 个 CompletionStage 子任务,该子任务需要接收一个参数 x,然后会输出一个结果——x 的平方值。 (3)“y -> System.out.println(y)”是一个 Comsumer 类型的 Lamda 表达式,被 thenAccept 方法包装成了一个 CompletionStage 子任务,该子任务需要消耗上一个 Stage(子任务)的输出值, 但是此 Stage 并没有输出。 (4)“() -> System.out.println() ”是一个 Runnable 类型的 Lamda 表达式,被 thenRun 方法包装成了一个 CompletionStage 子任务,既不消耗上一个 Stage 的输出,也不产生结果。 CompletionStage 代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另一个阶段。虽然一个 Stage 可以触发其他 Stage,但是并不能保证后续 Stage 的执行顺序。 ​

使用runAsync和supplyAsync创建子任务

CompletionStage 子任务的创建是通过 CompletableFuture 完成的。CompletableFuture 类提供 了非常强大的 Future 的扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合 CompletionStage 的方法。 CompletableFuture 定义了一组方法用于创建 CompletionStage 子任务(或者阶段性任务),基础的方法如下: ​

//子任务包装一个 Runnable 实例,并使用 ForkJoinPool.commonPool()线程池去执行 public static CompletableFuture runAsync(Runnable runnable) //子任务包装一个 Runnable 实例,并使用指定的 executor 线程池去执行 public static CompletableFuture runAsync( Runnable runnable, Executor executor) //子任务包装一个 Supplier 实例,并使用 ForkJoinPool.commonPool()线程池去执行 public static CompletableFuture supplyAsync( Supplier supplier) //子任务包装一个 Supplier 实例,并使用指定的 executor 线程池去执行 public static CompletableFuture supplyAsync( Supplier supplier, Executor executor)

在 CompletableFuture 创建 CompletionStage 子任务时,如果没有指定 Executor 线程池,默认情况下 CompletionStage 会使用公共的 ForkJoinPool 线程池。 下面是两个简单的创建 CompletionStage 子任务的演示用例: ​

//无返回值异步调用 @Test public void runAsyncDemo() throws Exception { CompletableFuture future = CompletableFuture.runAsync(() -> { sleepSeconds(1);//模拟执行1秒 Print.tco("run end ..."); }); //等待异步任务执行完成,现时等待2秒 future.get(2, TimeUnit.SECONDS); } //有返回值异步调用 @Test public void supplyAsyncDemo() throws Exception { CompletableFuture future = CompletableFuture.supplyAsync(() -> { long start = System.currentTimeMillis(); sleepSeconds(1);//模拟执行1秒 Print.tco("run end ..."); return System.currentTimeMillis() - start; }); //等待异步任务执行完成,现时等待2秒 long time = future.get(2, TimeUnit.SECONDS); Print.tco("异步执行耗时(秒) = " + time / 1000); } 设置子任务回调钩子

可以为 CompletionStage 子任务设置特定的回调钩子,当的计算结果完成,或者抛出异常的 时候,可以执行这些特定的回调钩子。 设置的子任务回调钩子的函数,主要是下面的方法: ​

//设置的子任务完成时的回调钩子 public CompletableFuture whenComplete( BiConsumer action) //设置的子任务完成时的回调钩子,可能不在同一线程执行 public CompletableFuture whenCompleteAsync( BiConsumer action) //设置的子任务完成时的回调钩子,提交给线程池 executor 执行 public CompletableFuture whenCompleteAsync( BiConsumer action, Executor executor) //设置的异常处理的回调钩子 public CompletableFuture exceptionally( Function fn)

下面是一个简单的为 CompletionStage 子任务设置完成钩子和异常钩子的演示用例:

@Test public void whenCompleteDemo() throws Exception { CompletableFuture future = CompletableFuture.runAsync(() -> { sleepSeconds(1);//模拟执行1秒 Print.tco("抛出异常!"); throw new RuntimeException("发生异常"); //Print.tco("run end ..."); }); //设置执行完成后的回调钩子 future.whenComplete(new BiConsumer() { @Override public void accept(Void t, Throwable action) { Print.tco("执行完成!"); } }); //设置发生异常后的回调钩子 future.exceptionally(new Function() { @Override public Void apply(Throwable t) { Print.tco("执行失败!" + t.getMessage()); return null; } }); future.get(); }

调用 cancel()方法取消 CompletableFuture 时,任务被视为异常完成,completeExceptionally() 方法所设置的异常回调钩子也会被执行到。 如果没有设置异常回调钩子,发生内部异常时,会有两种情况发生: (1)在使用 get()和 get(long, TimeUnit)方法启动任务时,如果遇到内部异常,则 get 方法会 抛出 ExecutionException(执行异常) (2)在使用 join()和 getNow(T)启动任务时(大多数情况下都是如此),如果遇到内部异常,join()和 getNow(T)方法会抛出 CompletionException。 ​

handler统一处理异常和结果

除了通过 whenComplete、exceptionally 设置完成钩子、异常钩子之外,还可以使用 handle 方 法统一处理结果和异常。 handle 方法有三个重载版本,三个版本的声明如下: ​

//在执行任务的同一个线程中处理异常和结果 public CompletionStage handle( BiFunction fn); //可能不在执行任务的同一个线程中处理异常和结果 public CompletionStage handleAsync( BiFunction fn); //在指定线程池 executor 中处理异常和结果 public CompletionStage handleAsync( BiFunction fn, Executor executor);

handle 方法的示例代码,具体如下: ​

@Test public void handleDemo() throws Exception { CompletableFuture future = CompletableFuture.runAsync(() -> { sleepSeconds(1);//模拟执行1秒 Print.tco("抛出异常!"); throw new RuntimeException("发生异常"); //Print.tco("run end ..."); }); //设置执行完成后的回调钩子 future.handle(new BiFunction() { @Override public Void apply(Void input, Throwable throwable) { if (throwable == null) { Print.tcfo("没有发生异常!"); } else { Print.tcfo("sorry,发生了异常!"); } return null; } }); future.get(); } 线程池的使用

默认情况下通过静态方法 runAsync 、supplyAsync 创建的 CompletableFuture 任务会使用公共的 ForkJoinPool 线程池,其默认的线程数是 CPU 的核数。当然,其线程数可以通过以下 JVM参数去设置:

option:-Djava.util.concurrent.ForkJoinPool.common.parallelism

问题是:如果所有 CompletableFuture 共享一个线程池,那么一旦有任务执行一些很慢的 IO操作,就会导致线程池中所有线程都阻塞在 IO 操作上,从而造成线程饥饿,进而影响整个系统 的性能。所以,强烈建议大家根据不同的业务类型创建不同的线程池,以避免互相干扰。 所以,建议大家在生产环境使用时,根据不同的业务类型创建不同的线程池,以避免互相影响。前面第一章为大家介绍了三种线程池:IO 密集型任务线程池、CPU 密集型任务线程池、混合 型任务线程池。大家可以根据不同的任务类型,确定线程池的类型和线程数。 作为演示,这里使用“混合型任务线程池”执行 CompletableFuture 任务,具体的代码如下: ​

//有返回值异步调用 @Test public void threadPoolDemo() throws Exception { //业务线程池 ThreadPoolExecutor pool= ThreadUtil.getMixedTargetThreadPool(); CompletableFuture future = CompletableFuture.supplyAsync(() -> { Print.tco("run begin ..."); long start = System.currentTimeMillis(); sleepSeconds(1);//模拟执行1秒 Print.tco("run end ..."); return System.currentTimeMillis() - start; },pool); //等待异步任务执行完成,现时等待2秒 long time = future.get(2, TimeUnit.SECONDS); Print.tco("异步执行耗时(秒) = " + time / 1000); } 异步任务的串行执行

如果两个异步任务需要串行(当一个任务依赖另一个任务)执行,可以通过 CompletionStage 接口的 thenApply、thenAccept、thenRun 和 thenCompose 四个方法实现。 ​

thenApply方法

thenApply 方法有三个重载版本,三个版本的声明如下: ​

//后一个任务与前一个任务在同一个线程中执行 public CompletableFuture thenApply(Function fn) //后一个任务与前一个任务可以不在同一个线程中执行 public CompletableFuture thenApplyAsync( Function fn) //后一个任务在指定的 executor 线程池中执行 public CompletableFuture thenApplyAsync(Function fn, Executor executor)

thenApply 三个重载版本有一个共同的参数 fn,该参数表示待串行执行的第二个异步任务, 其类型为 Function。fn 的类型声明涉及到两个范型参数,具体如下:

范型参数 T:上一个任务所返回结果的类型。范型参数 U:当前任务的返回值类型。

作为示例,使用 thenApply 分两步计算(10*10)*10,代码如下:

@Test public void test6() throws ExecutionException, InterruptedException { CompletableFuture longCompletableFuture = CompletableFuture.supplyAsync(new Supplier() { @Override public Long get() { return 10l * 10l; } }).thenApply(new Function() { @Override public Long apply(Long aLong) { return aLong * 10; } }); Long aLong = longCompletableFuture.get(); System.out.println("result:"+aLong); }

image.png

thenRun方法

thenRun 与 thenApply 方法不一样的是,不关心任务的处理结果。只要前一个任务执行完成, 就开始执行后一个串行任务。 ​

thenApply 方法也有三个重载版本,三个版本的声明如下:

//后一个任务与前一个任务在同一个线程中执行 public CompletionStage thenRun(Runnable action); //后一个任务与前一个任务可以不在同一个线程中执行 public CompletionStage thenRunAsync(Runnable action); //后一个任务在 executor 线程池中执行 public CompletionStage thenRunAsync(Runnable action,Executor executor);

从方法的声明可以看出,thenRun 方法同 thenApply 方法类似;不同的:前一个任务处理完成后,thenRun 并不会把计算的结果传给后一个任务,而且后一个任务也没有结果输出。 thenRun 系列方法里的 action 参数是 Runnable 类型,所以 thenRun 既不能接收参数也不支持返回值。 ​

thenAccept方法

thenAccept 折衷了 thenRun、thenApply 的特点,使用此方法,后一个任务可以接收(或消费) 前一个任务的处理结果,但是后一个任务没有结果输出。 thenAccept 方法有三个重载版本,三个版本的声明如下: ​

//后一个任务与前一个任务在同一个线程中执行 public CompletionStage thenAccept(Consumer action); //后一个任务与前一个任务可以不在同一个线程中执行 public CompletionStage thenAcceptAsync(Consumer action); //后一个任务在指定的 executor 线程池中执行 public CompletionStage thenAcceptAsync(Consumer action,Executor executor);

thenAccept 系列函数的回调参数为 action,其类型为 Consumer



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有