异步回调模式
随着业务模块系统越来越多,各个系统的业务架构变得越来越错综复杂,特别是随着这几年微服务架构的兴起,跨机器、跨服务的接口调用越来越频繁。打个简单的比方:现在的一个业务流程可能需要调用N次第三方接口,获取N种上游数据。因此,面临一个大的问题:如何异步去调取这些接口(做到高效率),然后同步去处理这些接口的返回结果呢?这里涉及线程的异步回调问题,这也是高并发的一个基础问题。在Netty源码中大量的使用了异步回调技术,并且基于Java的异步回调设计了自己的一整套异步回调接口和实现。
从泡茶的案例说起
在进入异步回调的正式解读之前,先看一个比较好理解的异步生活实例。这里使用阻塞模式和异步回调模式分别实现异步泡茶流程。
为了异步执行整个泡茶流程,分别设计三条线程:泡茶线程(MainThread,主线程)、烧水线程(HotWarterThread)、清洗线程(WashThread)。泡茶线程的工作是:启动清洗线程、启动烧水线程,等清洗、烧水的工作完成后,泡茶喝;清洗线程的工作是:洗茶壶、洗茶杯;烧水线程的工作是:洗好水壶,灌上凉水,放在火上,一直等水烧开。
join:异步阻塞之闷葫芦
阻塞模式实现泡茶实例首先从基础的多线程join合并实验入手。join操作的原理是阻塞当前的线程,直到待合并的目标线程的执行完成。
线程的合并流程
Java中线程的合并流程是:假设线程A调用线程B的join()方法去合并B线程,那么线程A进入阻塞状态,直到线程B执行完成。
在泡茶的例子中,主线程通过分别调用烧水线程和清洗线程的join()方法,等待烧水线程和清洗线程执行完成,然后执行主线程自己的泡茶操作。具体的执行流程如图所示:
使用join()实现泡茶喝是一个异步阻塞版本,具体的代码实现如下:
public class JoinDemo {
public static final int SLEEP_GAP = 500;
public static String getCurThreadName() {
return Thread.currentThread().getName();
}
static class HotWarterThread extends Thread {
public HotWarterThread() {
super("** 烧水-Thread");
}
public void run() {
try {
Logger.info("洗好水壶");
Logger.info("灌上凉水");
Logger.info("放在火上");
//线程睡眠一段时间,代表烧水中
Thread.sleep(SLEEP_GAP);
Logger.info("水开了");
} catch (InterruptedException e) {
Logger.info(" 发生异常被中断.");
}
Logger.info(" 运行结束.");
}
}
static class WashThread extends Thread {
public WashThread() {
super("$$ 清洗-Thread");
}
public void run() {
try {
Logger.info("洗茶壶");
Logger.info("洗茶杯");
Logger.info("拿茶叶");
//线程睡眠一段时间,代表清洗中
Thread.sleep(SLEEP_GAP);
Logger.info("洗完了");
} catch (InterruptedException e) {
Logger.info(" 发生异常被中断.");
}
Logger.info(" 运行结束.");
}
}
public static void main(String args[]) {
Thread hThread = new HotWarterThread();
Thread wThread = new WashThread();
hThread.start();
wThread.start();
//在等待烧水和清洗之时,可以干点其他事情
try {
//合并烧水-线程
hThread.join();
//合并清洗-线程
wThread.join();
Thread.currentThread().setName("主线程");
Logger.info("泡茶喝");
} catch (InterruptedException e) {
Logger.info(getCurThreadName() + "发生异常被中断.");
}
Logger.info(getCurThreadName() + " 运行结束.");
}
}
程序中有三个线程:主线程main、烧水线程hThread和清洗线程wThread。main调用了hThread.join()实例方法,合并烧水线程,也调用了wThread.join()实例方法,合并清洗线程。
说明一下:hThread、wThread是线程实例,在例子代码中,hThread对应的线程名称为“ 烧水-Thread”,wThread对应的线程名称为“清洗-Thread”。
join()方法详解
join()方法应用场景如下:
A线程调用B线程的join()方法,等待B线程执行完成;在B线程没有完成前,A线程阻塞。join()方法是有三个重载版本:
- void join():A线程等待B线程执行结束后,A线程重启执行。
- void join(long millis):A线程等待B线程执行一段时间,最长等待时间为millis(毫秒)。超过millis后,不论B线程是否结束,A线程重启执行。
- void join(long millis,int nanos):等待乙方线程执行一段时间,最长等待时间为millis加nanos
(纳秒)。超过该时间后,无论乙方是否结束,甲方线程都重启执行。
强调一下容易混淆的几点:
- join()是实例方法不是静态方法,需要使用线程对象去调用,如thread.join()。
- 调用join()时,不是thread所指向的目标线程阻塞,而是当前线程阻塞。
- 只有等到thread所指向的线程执行完成或者超时,当前线程才能启动执行。
join()有一个问题:被合并线程没有返回值。比如,在烧水的实例中,如果烧水线程的执行结束,main线程是没有办法知道结果的。同样,清洗线程的执行结果,main线程(泡茶线程)也是没有办法知道的。形象地说,join线程合并就像一个闷葫芦。只能发起合并线程,不能取到执行结果。如果需要获得异步线程的执行结果,怎么办呢?可以使用Java的FutureTask系列类。
join()的实现原理是不停地检查join线程是否存活,如果join线程存活,wait(0)就永远等下去,直至join线程终止后,线程的this.notifyAll()方法会被调用(该方法是在JVM中实现的,JDK中并不会看到源码),join()方法将退出循环,恢复主线程执行。很显然这种循环检查的方式比较低效。除此之外,调用join()缺少很多灵活性,比如实际项目中很少让自己单独创建线程,而是使用
Executor,这进一步减少了join()的使用场景,所以join()的使用多数停留在Demo演示上。
FutureTask:异步调用之重武器
为了获取异步线程的返回结果,Java在1.5版本之后提供了一种新的多线程的创建方式——FutureTask方式。FutureTask方式包含了一系列的Java相关的类,处于java.util.concurrent包中。使用FutureTask方式进行异步调用时,所涉及的重要组件为FutureTask类和Callable接口。由于Runnable有一个重要的问题,其run()方法是没有返回值的,因此Runnable不能用在需要有返回值的场景。为了解决Runnable接口的问题,Java定义了一个新的和Runnable类似的接口——Callable接口,并且将其中被异步执行的业务处理抽象方法——run()方法改名为call(),但是call()方法有返回值。
通过 FutureTask 获取异步执行结果的步骤
通过FutureTask类和Callable接口的联合使用可以创建能获取异步执行结果的线程。具体的步骤重复介绍如下:
- 创建一个Callable接口的实现类,并实现其call()方法,编写好异步执行的具体逻辑,并且可以有返回值。
- 使用Callable实现类的实例构造一个FutureTask实例。
- 使用FutureTask实例作为Thread构造器的target入参,构造新的Thread线程实例。
- 调用Thread实例的start()方法启动新线程,启动新线程的run()方法并发执行。其内部的执行过程为:启动Thread实例的run()方法并发执行后,会执行FutureTask实例的run()方法,最终会并发执行Callable实现类的call()方法。
- 调用FutureTask对象的get()方法阻塞性地获得并发线程的执行结果。
使用 FutureTask 实现异步泡茶喝
前面的join版本喝茶实例中有一个很大的问题:就是主线程获取不到异步线程的返回值。打个比方,如果烧水线程出了问题,或者清洗线程出了问题,main线程(泡茶线程)没有办法知道。哪怕不具备泡茶条件,main线程(泡茶线程)也只能继续泡茶喝。
使用FutureTask实现异步泡茶喝,main线程可以获取烧水线程、清洗线程的执行结果,然后根据结果判断是否具备泡茶条件,如果具备泡茶条件再泡茶。
使用FutureTask实现异步泡茶喝的执行流程具体如图所示:
使用FutureTask类和Callable接口进行泡茶喝的实战,代码如下:
public class JavaFutureDemo {
public static final int SLEEP_GAP = 500;
public static String getCurThreadName() {
return Thread.currentThread().getName();
}
static class HotWarterJob implements Callable<Boolean> //①
{
@Override
public Boolean call() throws Exception //②
{
try {
Logger.info("洗好水壶");
Logger.info("灌上凉水");
Logger.info("放在火上");
//线程睡眠一段时间,代表烧水中
Thread.sleep(SLEEP_GAP);
Logger.info("水开了");
} catch (InterruptedException e) {
Logger.info(" 发生异常被中断.");
return false;
}
Logger.info(" 运行结束.");
return true;
}
}
static class WashJob implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
try {
Logger.info("洗茶壶");
Logger.info("洗茶杯");
Logger.info("拿茶叶");
//线程睡眠一段时间,代表清洗中
Thread.sleep(SLEEP_GAP);
Logger.info("洗完了");
} catch (InterruptedException e) {
Logger.info(" 清洗工作 发生异常被中断.");
return false;
}
Logger.info(" 清洗工作
运行结束.");
return true;
}
}
public static void drinkTea(boolean warterOk, boolean cupOk) {
if (warterOk && cupOk) {
Logger.info("泡茶喝");
} else if (!warterOk) {
Logger.info("烧水失败,没有茶喝了");
} else if (!cupOk) {
Logger.info("杯子洗不了,没有茶喝了");
}
}
public static void main(String args[]) {
Thread.currentThread().setName("主线程");
Callable<Boolean> hJob = new HotWarterJob();//③
FutureTask<Boolean> hTask =
new FutureTask<>(hJob);//④
Thread hThread = new Thread(hTask, "** 烧水-Thread");//⑤
Callable<Boolean> wJob = new WashJob();//③
FutureTask<Boolean> wTask =
new FutureTask<>(wJob);//④
Thread wThread = new Thread(wTask, "$$ 清洗-Thread");//⑤
hThread.start();
wThread.start();
//在等待烧水和清洗时可以干点其他事情
try {
boolean
warterOk = hTask.get();
boolean
cupOk = wTask.get();
drinkTea(warterOk, cupOk);
} catch (InterruptedException e) {
Logger.info(getCurThreadName() + "发生异常被中断.");
} catch (ExecutionException e) {
e.printStackTrace();
}
Logger.info(getCurThreadName() + " 运行结束.");
}
}
首先,在上面的喝茶实例代码使用了Callable接口来替代Runnable接口,并且在call()方法中返回了异步线程的执行结果。
static class WashJob implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
//业务代码,并且有执行结果返回
}
}
其次,从Callable异步逻辑到异步线程需要创建一个FutureTask实例,并通过FutureTask实例创建新的线程:
Callable<Boolean> hJob = new HotWarterJob();//异步逻辑
FutureTask<Boolean> hTask = new FutureTask<Boolean>(hJob); //包装异步逻辑的异步任务实例
Thread hThread = new Thread(hTask, "烧水-Thread");//异步线程
FutureTask和Callable都是泛型类,泛型参数表示返回结果的类型。所以,在使用时它们两个实例的泛型参数需要保持一致。
最后,通过FutureTask实例取得异步线程的执行结果。一般来说,通过FutureTask实例的get()方法可以获取线程的执行结果。
总之,FutureTask比join线程合并操作更加高明,能取得异步线程的结果。但是,也就未必高明到哪里去。为什么呢?
因为通过FutureTask的get()方法获取异步结果时,主线程也会被阻塞。这一点FutureTask和join是一致的,它们都是异步阻塞模式。
异步阻塞的效率往往比较低,被阻塞的主线程不能干任何事情,唯一能干的就是傻傻等待。原生Java API除了阻塞模式的获取结果外,并没有实现非阻塞的异步结果获取方法。如果需要用到获取异步的结果,得引入一些额外的框架。
异步回调与异步阻塞调用
在前面的泡茶喝实例中,无论主线程调用join()进行闷葫芦式线程同步,还是使用Future.get()去获取异步线程的执行结果,都属于异步阻塞的调用。
异步阻塞属于主动模式的异步调用;异步回调属于被动模式的异步调用。在前面的异步阻塞版本的泡茶喝的实现中,泡茶线程是调用线程,烧水(或者清洗)线程是被调用线程,调用线程和被调用线程之间是一种主动关系,而不是被动关系。泡茶线程需要主动获取烧水(或者清洗)线程的执行结果。调用join()或Future.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方法的参数被传入。
-
FutureCallback的源码如下:
package com.google.common.util.concurrent;
public interface FutureCallback<V> {
void onSuccess(@Nullable V var1);
void onFailure(Throwable var1);
}
注意,Guava的FutureCallback与Java的Callable名字相近,实质不同,存在本质的区别:
- Java的Callable接口代表的是异步执行的逻辑。
- Guava的FutureCallback接口代表的是Callable异步逻辑执行完成之后,根据成功或者异常两种情形所需要执行的善后工作。
Guava是对Java Future异步回调的增强,使用Guava异步回调也需要用到Java的Callable接口。简单地说,只有在Java的Callable任务执行结果出来后,才可能执行Guava中的FutureCallback结果回调。Guava如何实现异步任务Callable和结果回调FutureCallback之间的监控关系呢?Guava引入了一个新接口ListenableFuture,它继承了Java的Future接口,增强了被监控的能力。
详解 ListenableFuture
Guava的ListenableFuture接口是对Java的Future接口的扩展,可以理解为异步任务实例,源码如下:
public interface ListenableFuture<V> extends Future<V> {
//此方法由Guava内部调用
void addListener(Runnabler, Executore);
}
ListenableFuture仅仅增加了一个addListener()方法。它的作用就是将FutureCallback善后 回 调 逻 辑 封 装 成 一 个 内 部 的 Runnable 异 步 回 调 任 务 , 在 Callable 异 步 任 务 完 成 后 回 调FutureCallback善后逻辑。
注意,此addListener()方法只在Guava内部使用。在实际编程中,addListener()不会使用到。
在实际编程中,如何将FutureCallback回调逻辑绑定到异步的ListenableFuture任务呢?可以使用Guava的Futures工具类,它有一个addCallback()静态方法,可以将FutureCallback的回调实例绑定到ListenableFuture异步任务。下面是一个简单的绑定实例:
Futures.addCallback(listenableFuture,
new FutureCallback<Boolean>()
{
public void onSuccess(Boolean r)
{
// listenableFuture内部的Callable成功时回调此方法
}
public void onFailure(Throwable t)
{
// listenableFuture内部的Callable异常时回调此方法
}
});
现在的问题来了,既然Guava的ListenableFuture接口是对Java的Future接口的扩展,两者都表示异步任务,那么Guava的异步任务实例从何而来?
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<Boolean> hFuture = gPool.submit(hJob);
//绑定回调实例
Futures.addCallback(listenableFuture,
new FutureCallback<Boolean>()
{
//有两种实现回调的方法
});
取到了ListenableFuture实例后,通过Futures.addCallback()方法将FutureCallback回调逻辑的实例绑定到ListenableFuture异步任务实例,实现异步执行完成后的回调。
总结一下,Guava异步回调的流程如下:
- 实现Java的Callable接口,创建的异步执行逻辑。还有一种情况,如果不需要返回值,异步执行逻辑也可以实现Runnable接口。
- 创建Guava线程池。
- 将1创建的Callable/Runnable异步执行逻辑的实例提交到Guava线程池,从而获取ListenableFuture异步任务实例。
- 创建FutureCallback回调实例,通过Futures.addCallback将回调实例绑定到ListenableFuture异步任务上。
完成以上4步,当Callable/Runnable异步执行逻辑完成后,就会回调异步回调实例FutureCallback实例的回调方法onSuccess()/onFailure()。
使用 Guava 实现泡茶喝的实例
基于Guava异步回调模式的泡茶喝程序的执行流程如图所示。
下面是基于Guava异步回调的泡茶喝程序演进版本,代码如下:
public class GuavaFutureDemo
{
public static final int SLEEP_GAP = 3000;
static class HotWaterJob implements Callable<Boolean> //①
{
@Override
public Boolean call() throws Exception //②
{
try
{
Print.tcfo("洗好水壶");
Print.tcfo("烧开水");
//线程睡眠一段时间,代表烧水中
Thread.sleep(SLEEP_GAP);
Print.tcfo("水开了");
} catch (InterruptedException e)
{
Print.tcfo(" 发生异常被中断.");
return false;
}
Print.tcfo(" 烧水工作,运行结束.");
return true;
}
}
static class WashJob implements Callable<Boolean>
{
@Override
public Boolean call() throws Exception
{
try
{
Print.tcfo("洗茶杯");
//线程睡眠一段时间,代表清洗中
Thread.sleep(SLEEP_GAP);
Print.tcfo("洗完了");
} catch (InterruptedException e)
{
Print.tcfo(" 清洗工作发生异常被中断.");
return false;
}
Print.tcfo(" 清洗工作运行结束.");
return true;
}
}
//泡茶喝的工作
static class DrinkJob
{
boolean waterOk = false;
boolean cupOk = false;
//泡茶喝,回调方法
public void drinkTea()
{
if (waterOk && cupOk)
{
Print.tcfo("泡茶喝,茶喝完");
this.waterOk = false;
}
}
}
public static void main(String args[])
{
Thread.currentThread().setName("泡茶喝线程");
//新起一个线程,作为泡茶主线程
DrinkJob drinkJob = new DrinkJob();
//烧水的业务逻辑
Callable<Boolean> hotJob = new HotWaterJob();
//清洗的业务逻辑
Callable<Boolean> washJob = new WashJob();
//创建Java线程池
ExecutorService jPool =
Executors.newFixedThreadPool(10);
//包装Java线程池,构造Guava线程池
ListeningExecutorService gPool =
MoreExecutors.listeningDecorator(jPool);
//烧水的回调钩子
FutureCallback<Boolean> hotWaterHook = new FutureCallback<Boolean>()
{
public void onSuccess(Boolean r)
{
if (r)
{
drinkJob.waterOk = true;
//执行回调方法
drinkJob.drinkTea();
}
}
public void onFailure(Throwable t)
{
Print.tcfo("烧水失败,没有茶喝了");
}
};
//启动烧水线程
ListenableFuture<Boolean> hotFuture = gPool.submit(hotJob);
//设置烧水任务的回调钩子
Futures.addCallback(hotFuture, hotWaterHook);
//启动清洗线程
ListenableFuture<Boolean> washFuture = gPool.submit(washJob);
//使用匿名实例,作为清洗之后的回调钩子
Futures.addCallback(washFuture, new FutureCallback<Boolean>()
{
public void onSuccess(Boolean r)
{
if (r)
{
drinkJob.cupOk = true;
//执行回调方法
第 9 章
异步回调模式
|
431
drinkJob.drinkTea();
}
}
public void onFailure(Throwable t)
{
Print.tcfo("杯子洗不了,没有茶喝了");
}
});
Print.tcfo("干点其他事情...");
sleepSeconds(1);
Print.tcfo("执行完成");
}
}
Guava 异步回调和 Java 异步调用的区别
总结一下Guava异步回调和Java的FutureTask异步调用的区别,具体如下:
- FutureTask是主动调用的模式,“调用线程”主动获得异步结果,在获取异步结果时处于阻塞状态,并且会一直阻塞,直到拿到异步线程的结果。
- Guava是异步回调模式,“调用线程”不会主动去获得异步结果,而是准备好回调函数,并设置好回调钩子;执行回调函数的并不是“调用线程”自身,回调函数的执行者是“被调用线程”,“调用线程”在执行完自己的业务逻辑后就已经结束了。当回调函数被执行时,“调用线程”已经结束很久了。
为什么将FutureTask称为异步调用之重武器呢?这里为大家揭晓答案。主要有两个原因:
- 和异步回调模式相比,使用FutureTask获取结果时,调用线程(如泡茶线程)多少存在阻塞;
- 使用FutureTask又涉及三四个类或接口的使用,与join相比,使用起来烦琐多了。所以,本书特将其称为异步调用之重武器。
Netty 的异步回调模式
Netty官方文档说明Netty的网络操作都是异步的。Netty源码中大量使用了异步回调处理模式。在Netty的业务开发层面,处于Netty应用的Handler处理程序中的业务处理代码也都是异步执行的。所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是一致的。对应关系为:
- Netty的Future接口可以对应到Guava的ListenableFuture接口。
- Netty的GenericFutureListener接口可以对应到Guava的FutrueCallback接口。
GenericFutureListener 接口详解
前面提到,和Guava的FutrueCallback一样,Netty新增了一个接口,用来封装异步非阻塞回调的逻辑,那就是GenericFutureListener接口。GenericFutureListener位于io.netty.util.concurrent包中,源码如下:
public interface GenericFutureListener<F extends Future<?>> extends EventListener {
//监听器的回调方法
void operationComplete(F var1) throws Exception;
}
GenericFutureListener拥有一个回调方法operationComplete(),表示异步任务操作完成。在Future异 步 任 务 执 行 完 成 后 将 回 调 此 方 法 。 大 多 数 情 况 下 , Netty 的 异 步 回 调 的 代 码 编 写 在GenericFutureListener接口的实现类中的operationComplete()方法中。说明一下,GenericFutureListener的父接口EventListener是一个空接口,没有任何抽象方法,是一个仅仅具有标识作用的接口。
Netty 的 Future 接口详解
Netty也对Java的Future接口进行了扩展,并且名称没有变,还是被称为Future接口,实现在io.netty.util.concurrent包中。
和Guava的ListenableFuture一样,Netty的Future接口扩展了一系列方法,对执行的过程进行监控,对异步回调完成事件进行Listen监听并且回调。Netty的Future的源码如下:
public interface Future<V>
extends
java.util.concurrent.Future<V> {
boolean isSuccess();
//判断异步执行是否成功
boolean isCancellable();
//判断异步执行是否取消
Throwable cause();
//获取异步任务异常的原因
//增加异步任务执行完成Listener监听器
Future<V> addListener(GenericFutureListener<? extends Future<? super V>> listener);
//移除异步任务执行完成Listener监听器
Future<V> removeListener(GenericFutureListener<? extends Future<? super V>> listener);
...
}
Netty的Future接口一般不会直接使用,使用过程中会使用其他的子接口。Netty有一系列的子接口,代表不同类型的异步任务,如ChannelFuture接口。ChannelFuture子接口表示Channel通道I/O操作的异步任务;如果在Channel的异步I/O操作完成后,需要执行回调操作,就需要使用到ChannelFuture接口。
ChannelFuture 的使用
在Netty网络编程中,网络连接通道的输入、输出处理都是异步进行的,都会返回一个ChannelFuture接口的实例。通过返回的异步任务实例,可以为其增加异步回调的监听器。在异步任务真正完成后,回调执行。
Netty的网络连接的异步回调,实例代码如下:
//connect是异步的,仅仅是提交异步任务
ChannelFuture future = bootstrap.connect(
new InetSocketAddress("www.manning.com",80));
//connect的异步任务真正执行完成后,future回调监听器会执行
future.addListener(new ChannelFutureListener()
{
@Override
public void operationComplete(ChannelFuture channelFuture)
throws Exception
{
if(channelFuture.isSuccess()){
System.out.println("Connection established");
}
else
{
System.err.println("Connection attempt failed");
channelFuture.cause().printStackTrace();
}
}
});
GenericFutureListener接口在Netty中是一个基础类型接口。在网络编程的异步回调中,一般使用Netty中提供的某个子接口,如ChannelFutureListener接口。在上面的代码中,使用到的是这个子接口。
Netty 的出站和入站异步回调
Netty的出站和入站操作都是异步的。这里异步回调的方法和前面Netty建立的异步回调是一样的。下面以经典的NIO出站操作write为例说明ChannelFuture的使用。在write操作调用后,Netty并没有立即完成对Java NIO底层连接的写入操作,底层的写入操作是异步执行的,代码如下:
//write()输出方法,返回的是一个异步任务
ChannelFuture future = ctx.channel().write(msg);
//为异步任务加上监听器
future.addListener(
new ChannelFutureListener()
{
@Override
public void operationComplete(ChannelFuture future)
{
//write操作完成后的回调代码
}
});
在write操作完成后立即返回,返回的是一个ChannelFuture接口的实例。通过这个实例可以绑定异步回调监听器,编写异步回调的逻辑。
如果大家运行以上的EchoServer案例会发现一个很大的问题:客户端接收到的回写信息和发送到服务器的信息不是一一对应输出的。看到的比较多的情况是:客户端发出很多次信息后,客户端才收到一次服务器的回写。这是什么原因呢?这就是网络通信中的粘包/半包问题。粘包/半包问题的出现说明了一个问题:仅仅基于Java的NIO开发一套高性能、没有Bug的通信服务器程序远远没有大家想象的简单,有一系列的坑、一大堆的基础问题等着大家解决。
在进行大型的Java通信程序的开发时,尽量采用一些实现了成熟、稳定的基础通信的Java开源中间件(如Netty)。这些中间件已经帮助大家解决了很多的基础问题,如前面出现的粘包/半包问题。
异步回调模式小结
随着高并发系统越来越多,异步回调模式愈发重要。在Netty源码中大量使用了异步回调技术。首先为大家介绍了Java的join闷葫芦式的异步阻塞,然后介绍了Java的FutureTask阻塞式地获取异步任务结果,最后介绍了Guava和Netty的异步回调方式。Guava和Netty的异步回调是非阻塞的,
感谢耐心看到这里的同学,觉得文章对您有帮助的话希望同学们不要吝啬您手中的赞,动动您智慧的小手,您的认可就是我创作的动力!
之后还会勤更自己的学习笔记,感兴趣的朋友点点关注哦。