【设计模式】异步阻塞、异步回调模式
1 前言
为什么要看这个异步回调呢?是因为我上节在看 RocektMQ 发送消息的时候,它支持同步、异步、一次性的模式,后两者不会阻塞当前线程,但是看这两者都没用到线程池,那它是如何处理的呢?我们看下三者最后的落点,都是在 NettyRemotingAbstract 这个类里:
// NettyRemotingAbstract#invokeSyncImpl public RemotingCommand invokeSyncImpl(Channel channel, RemotingCommand request, long timeoutMillis) throws InterruptedException, RemotingSendRequestException, RemotingTimeoutException { RemotingCommand var9; try { ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis, (InvokeCallback)null, (SemaphoreReleaseOnlyOnce)null); this.responseTable.put(opaque, responseFuture); SocketAddress addr = channel.remoteAddress(); // 设置回调监听 channel.writeAndFlush(request).addListener((f) -> { if (f.isSuccess()) { responseFuture.setSendRequestOK(true); } else { responseFuture.setSendRequestOK(false); this.responseTable.remove(opaque); responseFuture.setCause(f.cause()); // 触发 countDownLatch.countDown(); responseFuture.putResponse((RemotingCommand)null); log.warn("Failed to write a request command to {}, caused by underlying I/O operation failure", addr); } }); // 阻塞当前线程 countDownLatch.await(3秒) RemotingCommand responseCommand = responseFuture.waitResponse(timeoutMillis); if (null == responseCommand) { if (responseFuture.isSendRequestOK()) { throw new RemotingTimeoutException(RemotingHelper.parseSocketAddressAddr(addr), timeoutMillis, responseFuture.getCause()); } throw new RemotingSendRequestException(RemotingHelper.parseSocketAddressAddr(addr), responseFuture.getCause()); } var9 = responseCommand; } finally { this.responseTable.remove(opaque); } return var9; } // NettyRemotingAbstract#invokeAsyncImpl public void invokeAsyncImpl(Channel channel, RemotingCommand request, long timeoutMillis, InvokeCallback invokeCallback) throws InterruptedException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException { if (acquired) { SemaphoreReleaseOnlyOnce once = new SemaphoreReleaseOnlyOnce(this.semaphoreAsync); long costTime = System.currentTimeMillis() - beginStartTime; if (timeoutMillis < costTime) { once.release(); throw new RemotingTimeoutException("invokeAsyncImpl call timeout"); } else { ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis - costTime, invokeCallback, once); this.responseTable.put(opaque, responseFuture); try { // 设置回调监听 channel.writeAndFlush(request).addListener((f) -> { if (f.isSuccess()) { responseFuture.setSendRequestOK(true); } else { this.requestFail(opaque); log.warn("send a request command to channel <{}> failed.", RemotingHelper.parseChannelRemoteAddr(channel)); } }); } catch (Exception var15) { this.responseTable.remove(opaque); responseFuture.release(); log.warn("send a request command to channel <" + RemotingHelper.parseChannelRemoteAddr(channel) + "> Exception", var15); throw new RemotingSendRequestException(RemotingHelper.parseChannelRemoteAddr(channel), var15); } } } ... } // NettyRemotingAbstract#invokeOnewayImpl public void invokeOnewayImpl(Channel channel, RemotingCommand request, long timeoutMillis) throws InterruptedException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException { ... try { // 设置回调监听 channel.writeAndFlush(request).addListener((f) -> { once.release(); if (!f.isSuccess()) { log.warn("send a request command to channel <" + channel.remoteAddress() + "> failed."); } }); } catch (Exception var8) { ... } ... }
可以看到三种模式的处理,一次性以及异步的处理是一样的,都是添加上回调监听即可,channel.writeAndFlush(request) 返回的是 Netty 里的 ChannelFuture ,当异步处理完读写然后执行你的回调。而同步的里边,设置上监听,并且自己通过 CountDownLauch 来阻塞当前线程,当异步的回调里执行完成,countDown()后,当前线程才继续往下走。这里就用到了 Netty 里的异步回调,所以我们本节就看看异步回调以及跟他相关的异步阻塞。
2 基本概念
异步回调相对应的还有一个异步阻塞,异步阻塞属于主动模式的异步调用;异步回调属于被动模式的异步调用。
主动调用是一种阻塞式调用,“调用方”要等待“被调用方”执行完毕才返回。如果“被调 用方”的执行时间很长,那么“调用方”线程需要阻塞很长一段时间。而被动的调用模式,也就是说,被调用方在执行完成后,会反向执行“调用方”所设置的钩子方法。
主动调用比如我们熟悉的 join、FutureTask.get 都是会阻塞当前线程,异步回调比如 CompletableFuture、谷歌的 Guava Future相关技术、Netty的异步回调技术。
接下来我们就分别看下异步阻塞以及异步回调的一些实现。
3 异步阻塞
3.1 Join
join操作的原理是阻塞当前的线程,直到待合并的目标线程的执行完成。比如使用join()实现泡茶喝是一个异步阻塞版本,具体的代码实现如下:
public class JustDemo { @SneakyThrows public static void main(String[] args) { Thread thread = new Thread(() -> { System.out.println(Thread.currentThread().getName() + "在做事"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } }); thread.start(); // 等待 thread 做完事 thread.join(); // main 其他事 System.out.println("main 去做其他事情了"); } }
程序中有两个线程:thread线程和main线程,main线程调用了 thread 的 join 方法,相当于合并了 thread 线程,等 thread 线程执行完,自己才能继续执行其他事情。
关于 join 方法的详解可以参考我这篇:【线程基础】【四】join()方法详解
join()有一个问题:被合并线程没有返回值。join线程合并就像一个闷葫芦。只能发起合并线程,不能取到执行结果。并且它的等待机制如下:
public final synchronized void join(long millis)throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }
join()的实现原理是不停地检查join线程是否存活,如果join线程存活,wait(0)就永远等下去, 直至join线程终止后,线程的this.notifyAll()方法会被调用(该方法是在JVM中实现的,JDK中并不 会看到源码),join()方法将退出循环,恢复主线程执行。很显然这种循环检查的方式比较低效。
除此之外,调用join()缺少很多灵活性,比如实际项目中很少让自己单独创建线程,而是使用 Executor,这进一步减少了join()的使用场景,所以join()的使用多数停留在Demo演示上。
3.2 FutureTask
如果需要获得异步线程的执行结果,怎么办呢?可以使用Java的FutureTask系列类。那我们上边的例子可以这么写:
public class JustDemo { @SneakyThrows public static void main(String[] args) { FutureTask<Boolean> futureTask = new FutureTask<>(() -> { System.out.println(Thread.currentThread().getName() + "在做事"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } return Boolean.TRUE; }); Thread thread = new Thread(futureTask); thread.start(); Boolean result = futureTask.get(); System.out.println(result); // main 其他事 System.out.println("main 去做其他事情了"); } }
关于 FutureTask 的原理,这里就不复述了可以参考我的这篇:【Java 线程池】【六】线程池submit、Future、FutureTask原理
因为通过FutureTask的get()方法获取异步结果时,主线程也会被阻塞。这一点FutureTask和join是一致的,它们都是异步阻塞模式。那么接下来我们看看异步回调。
4 异步回调
4.1 CompletableFuture
Java8提供一个新的、具备异步回调能力的工具类 ——CompletableFuture,它是JDK 1.8引入的实现类,该类实现了Future和CompletionStage两个接口。该 类的实例作为一个异步任务,可以在自己异步执行完成之后触发一些其他的异步任务,从而达到异步回调的效果。
CompletableFuture的UML类关系图如下:
对于Future接口,大家已经非常熟悉了,接下来介绍一下CompletionStage接口。CompletionStage 代表异步计算过程中的某一个阶段,一个阶段完成以后可能会进入另一个阶段。一个阶段可以理解 为一个子任务,每个子任务会包装一个Java函数式接口实例,表示该子任务所要执行的操作。
CompletionStage代表某个同步或者异步计算的一个阶段或者是一系列异步任务中的一个子任务(或者阶段性任务)。
每个CompletionStage子任务所包装的可以是一个Function、Consumer或者Runnable函数式接口 实例。这三个常用的函数式接口的特点如下:
(1)Function
Function接口的特点是:有输入、有输出。包装了Function实例的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类型的Lambda表达式,被thenApply方法包装成了一个CompletionStage子任务,该子任务需要接收一个参数x,然后会输出一个结果——x的平方值。
(3)“y->System.out.println(y)”是一个Consumer类型的Lambda表达式,被thenAccept()方法包装成了一个CompletionStage子任务,该子任务需要上一个子任务的输出值,但是此子任务并没有输出。
(4)“()->System.out.println()”是一个Runnable类型的Lambda表达式,被thenRun()方法包装成了一个CompletionStage子任务,既不需要上一个子任务的输出值,又不产生结果。
CompletionStage代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另一个阶段。虽然一个子任务可以触发其他子任务,但是并不能保证后续子任务的执行顺序。
4.1.1 使用runAsync和supplyAsync 创建子任务
CompletionStage子任务的创建是通过CompletableFuture完成的。CompletableFuture类提供了非常强大的Future的扩展功能来帮助我们减少异步编程的复杂性,提供了函数式编程的能力来帮助我们通过回调的方式处理计算结果,也提供了转换和组合CompletionStage()的方法。CompletableFuture定义了一组方法用于创建CompletionStage子任务(或者阶段性任务),基础的方法如下:
//子任务包装一个Runnable实例,并使用ForkJoinPool.commonPool()线程池来执行 public static CompletableFuture<Void> runAsync(Runnable runnable) //子任务包装一个Runnable实例,并调用指定的executor线程池来执行 public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor) //子任务包装一个Supplier实例,并调用ForkJoinPool.commonPool()线程池来执行 public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) //子任务包装一个Supplier实例,并调用指定的executor线程池来执行 public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
在使用CompletableFuture创建CompletionStage子任务时,如果没有指定Executor线程池,在默认情况下CompletionStage会使用公共的ForkJoinPool线程池。
那么我们采用 CompletableFuture 的方式上边的例子,我们可以这么写:
public class JustDemo { @SneakyThrows public static void main(String[] args) { CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(() -> { System.out.println(Thread.currentThread().getName() + "在做事"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } return Boolean.TRUE; }); Boolean result = future.get(); System.out.println(result); // main 其他事 System.out.println("main 去做其他事情了"); } }
4.1.2 设置的子任务回调钩子
可以为CompletionStage子任务设置特定的回调钩子,当计算结果完成或者抛出异常的时候, 可以执行这些特定的回调钩子。
设置子任务回调钩子的主要函数如下:
//设置子任务完成时的回调钩子 public CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action) //设置子任务完成时的回调钩子,可能不在同一线程执行 public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action) //设置的子任务完成时的回调钩子,提交给线程池executor执行 public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor) //设置的异常处理的回调钩子 public CompletableFuture<T> exceptionally(Function<Throwable,? extends T> fn)
那我们上边的例子可以改写成:
public class JustDemo { @SneakyThrows public static void main(String[] args) { CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(() -> { System.out.println(Thread.currentThread().getName() + "在做事"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } return Boolean.TRUE; }); // 设置执行完毕的回调 future.whenComplete((res, e) -> { System.out.println(res); // main 其他事 System.out.println("main 去做其他事情了"); }); // 因为默认的公告线程池里的线程都是守护线程 所以我们这里等待一下 TimeUnit.SECONDS.sleep(2); } }
CompletableFuture 还有很多的玩法,我们这里就不一一列举,这里主要体现一下它的回调设置哈、
4.2 Guava Future
Guava是Google提供的Java扩展包,它提供了一种异步回调的解决方案。Guava中与异步回调相关的源码处于com.google.common.util.concurrent包中。包中的很多类都用于对java.util.concurrent的能力扩展和能力增强。比如,Guava的异步任务接口ListenableFuture扩展了Java的Future接口,实现了异步回调的的能力。
Guava主要增强了Java而不是另起炉灶。为了实现异步回调方式获取异步线程的结果,Guava做了以下增强:
引入了一个新的接口ListenableFuture,继承了Java的Future接口,使得Java的Future异步任务在Guava中能被监控和以非阻塞方式获取异步结果。
引入了一个新的接口FutureCallback, 这是一个独立的新接口。 该接口的目的是在异步任务执行完成后,根据异步结果完成不同的回调处理,并且可以处理异步结果。
FutureCallback是一个新增的接口,用来填写异步任务执行完后的监听逻辑。FutureCallback拥有两个回调方法:
public interface FutureCallback<V> { // 在异步任务执行成功后被回调。调用时,异步任务的执行结果作为onSuccess()方法的参数被传入 void onSuccess(@Nullable V result); // 在异步任务执行过程中抛出异常时被回调。调用时,异步任务所抛出的异常作为onFailure方法的参数被传入 void onFailure(Throwable t); }
回调任务怎么跟我们的异步任务关联呢?Guava引入了一个新接口ListenableFuture,它继承了Java的Future接口。
Guava的ListenableFuture接口是对Java的Future接口的扩展, 可以理解为异步任务实例:
public interface ListenableFuture<V> extends Future<V> { void addListener(Runnable listener, Executor executor); }
ListenableFuture仅仅增加了一个addListener()方法。它的作用就是将9.5.1节的FutureCallback善后 回 调 逻 辑 封 装 成 一 个 内 部 的 Runnable 异 步 回 调 任 务 , 在 Callable 异 步 任 务 完 成 后 回 调FutureCallback善后逻辑。
注意,此addListener()方法只在Guava内部使用,如果对它感兴趣,可以查看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异常时回调此方法 } }, executors);
现在的问题来了, 既然Guava的ListenableFuture接口是对Java的Future接口的扩展, 两者都表示异步任务,那么Guava的异步任务实例从何而来?
如果要获取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>(){ // 有两种实现回调的方法 }, gPool);
取到了ListenableFuture实例后, 通过Futures.addCallback()方法将FutureCallback回调逻辑的实例绑定到ListenableFuture异步任务实例,实现异步执行完成后的回调。
总结一下,Guava异步回调的流程如下:
(1)实现Java的Callable接口,创建的异步执行逻辑。还有一种情况,如果不需要返回值,异步执行逻辑也可以实现Runnable接口。
(2)创建Guava线程池。
(3)将 (1)创建的 Callable/Runnable 异步执行逻辑的实例提交到 Guava 线程池 , 从而获取ListenableFuture异步任务实例。
(4)创建FutureCallback回调实例,通过Futures.addCallback将回调实例绑定到ListenableFuture异步任务上。
完成以上4步, 当Callable/Runnable异步执行逻辑完成后, 就会回调异步回调实例FutureCallback实例的回调方法onSuccess()/onFailure()。
接下来我们再改一下上边的例子:
public class JustDemo { @SneakyThrows public static void main(String[] args) { // 创建 Java 里的线程池 ExecutorService executorService = Executors.newFixedThreadPool(10); // 用 Guava 将线程池包装一层 ListeningExecutorService listeningExecutorService = MoreExecutors.listeningDecorator(executorService); // 提交任务 ListenableFuture<Boolean> listenableFuture = listeningExecutorService.submit(() -> { System.out.println(Thread.currentThread().getName() + "在做事"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } return Boolean.TRUE; }); // 设置回调 Futures.addCallback(listenableFuture, new FutureCallback<Boolean>() { @Override public void onSuccess(@Nullable Boolean result) { if (Objects.nonNull(result) && result) { System.out.println("回掉到我了 去做其他事情了"); } } @Override public void onFailure(Throwable t) { System.out.println("失败了,,msg=" + t.getMessage()); } }, listeningExecutorService); System.out.println("main 继续"); } }
截图跟上边的不一样,是因为晚上回家在 mac上执行的哈:
总结一下Guava异步回调和Java的FutureTask异步调用的区别,具体如下:
(1)FutureTask是主动调用的模式,“调用线程”主动获得异步结果,在获取异步结果时处于阻塞状态,并且会一直阻塞,直到拿到异步线程的结果。
(2)Guava是异步回调模式,“调用线程”不会主动去获得异步结果,而是准备好回调函数,并设置好回调钩子; 执行回调函数的并不是 “调用线程” 自身, 回调函数的执行者是 “被调用线程”,“调用线程” 在执行完自己的业务逻辑后就已经结束了。当回调函数被执行时,“调用线程” 已经结束很久了。
4.3 Netty Future
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位于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也对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>> var1); Future<V> addListeners(GenericFutureListener<? extends Future<? super V>>... var1); // 移除异步任务执行完成Listener监听器 Future<V> removeListener(GenericFutureListener<? extends Future<? super V>> var1); Future<V> removeListeners(GenericFutureListener<? extends Future<? super V>>... var1); ... }
Netty的Future接口一般不会直接使用,使用过程中会使用其他的子接口。Netty有一系列的子接口,代表不同类型的异步任务,如ChannelFuture接口。
ChannelFuture子接口表示Channel通道I/O操作的异步任务;如果在Channel的异步I/O操作完成后,需要执行回调操作,就需要使用到ChannelFuture接口,这个也是我们上边看 RokcetMQ 发送消息的时候用到的异步回调机制。
在 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建立的异步回调是一样的。在write操作调用后,Netty并没有立即完成对Java NIO底层连接的写入操作,底层的写入操作是异步执行的,代码如下:
// write()输出方法,返回的是一个异步任务 ChannelFuture future = ctx.channel().write(msg); // 为异步任务加上监听器 future.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) { //write操作完成后的回调代码 } });
在write操作完成后立即返回,返回的是一个ChannelFuture接口的实例。通过这个实例可以绑定异步回调监听器,编写异步回调的逻辑。
5 小结
好啦,本节我们从异步阻塞的 Join、FutureTask 再到异步回掉的 CompletableFuture、谷歌的 Guava Future相关技术、Netty的异步回调大致看了看,有理解不对的地方还请指正哈。