Java网络编程中异步编程的理解

[TOC]html

前言

这篇文章主要是总结本身对于网络编程中异步,同步,阻塞和非阻塞的理解,这个问题自从学习NIO以来一直困扰着我,,其实想来好久就想写了,只不过当时理解不够,无从下手。最近在学习VertX框架,又去熟悉了下Netty的代码,由于了对于多线程也有了更深的理解,因此才开始对于这些概念有了理解,用于理清思路,本文须要有良好的多线程和网络编程基础,不适合初学者。node

1、异步,同步,阻塞和非阻塞的理解

关于这四个概念在IO方面的理解我贴两个连接,他们已经有了很好的说明我就再也不讲述:c++

  1. 怎样理解阻塞非阻塞与同步异步的区别? - 严肃的回答 - 知乎
  2. IO - 同步,异步,阻塞,非阻塞

之前在学习c++中muduo只是记得陈硕说的epoll是一个同步非阻塞的模型,可是网上不少人说Reactor模型是一个异步阻塞的模型,在学习Netty的时候官网是这么介绍的:web

Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.sql

Netty是一个异步的高性能网络框架,那么究竟是谁说错了?数据库

其实你们都没有错误,只是角度不一样。 先说说什么IO是异步的?异步实际上是针对数据从内核拷贝到用户进程空间这个操做是谁完成的,同步IO很是好理解,当用户进程发起一个read操做的时候发生一次系统调用,而后内核检查有没有数据,若是有则复制数据到进程空间,用户进程继续执行。而异步IO中复制数据到进程空间这个操做是内核帮你完成的,等完成以后再来通知你,执行你的逻辑。Reactor模型中,EventLoop线程在select到有可读数据以后,而后在本身去读取数据,因此从这个角度来说Reactor模型确实是同步的,在Linux的五种IO模型中只有异步IO是异步的。apache

那么为何Netty说他是一个异步网络库呢,这实际上是另外一个角度的阐述,对于网络库的做者来讲,他们面向的是Linux提供的这些api,因此说多路复用的Reactor是同步的没问题。那么对于Netty的使用者来讲,咱们面向的是Netty,Netty进一步封装了IO操做,在咱们发起IO操做的时候它返回了一个Future,咱们能够提供一个监听器来传入咱们的回调,当IO操做完成时会执行咱们的逻辑,咱们的这个操做相对于Netty就是异步的。编程

因此Reactor是同步非阻塞的,Netty是异步非阻塞的。segmentfault

2、异步编程从用户层面和框架层面不一样角度的理解

Java中的Future是异步的吗?api

对于这个问题,我想相信不少同窗都会认为是异步的,这里我认为是同步的,下面谈谈个人理解。
先想一想一个异步操做须要哪些元素,我认为须要发起者,执行者,执行逻辑,回调逻辑。流程: 发起者请求执行者去执行所需逻辑,而后在成功以后调用回调逻辑。Future中缺了什么?没错,就是那个回调!

咱们使用Future的模式通常是:投递一个任务到线程池得有个Future,而后去执行其余能够并行的操做,操做完以后去调用Future的get方法获取结果或者isDone判断是否执行完毕。这里的Future只是对于计算结果的一个建模,咱们在后面须要使用的时候再去轮询(轮询也是同步非阻塞的一个标志)或者阻塞,他提供的了一个很是好的特性:非阻塞!因此我认为Future是一个同步非阻塞的实现。也正是由于Future没有实现异步的特性,在jdk1.8以后新增了CompletableFuture提供了异步的特性。

注意异步元素的发起者和执行者能够是同一个线程,最多见的例子就是NodeJs的单线程模型。拿Netty的线程来具体,你在EventLoop中发起一个写请求后获得一个Future,你能够设置回调,下次执行这个回调的仍是EventLoop线程

用户角度的理解

这里主要说说在使用异步编程的一点理解,由于平时仍是用为主,咱们做为框架的使用者有必要了解一些常见的使用范式。就我目前接触的最多仍是CompletableFuture,Netty和VertX,当时也写过一点Js,Js主要也是回调的用法。我知道的用法以下:

  1. 回调 这种是最多见的,相信也是最容易理解的,Js和VertX不少都采用了这个实现,咱们在调用一个函数的时候提供一个响应结果的回调。响应式编程就是结合函数式和异步回调的一个产物,我相信之后会愈来愈常见
  2. 监听器 这个是Netty的实现,Netty将不少同步的地方改为了异步同时返回一个Future,咱们能够经过这个Future添加监听器,执行获得结果时的逻辑
  3. 组合式 相对于回调式,在实现多个回调时代码扁平化,能够了解下CompletableFuture的用法和实现真的是很是的优雅

由于异步的高性能,不少时候咱们本身也想把一个操做封装成异步的,就须要明白到底什么是异步,明白异步须要的元素,你会发现若是不借助之后的异步组件将一个操做封装成异步很是的困难,因此最简单的方案就是将你的回调最终传递到已有异步的组件中。

举2个简单的例子:

  1. 咱们利用CompletableFuture.supplyAsync(Object::new).thenAccept(o -> System.out.println(o));这一行很是简单的代码实现了一个异步,Object::new会被投递到线程池中,而后执行完成后执行打印语句。
  2. VertX的例子,VertX将不少同步的操做封装成了异步的操做,好比场景的发起Http请求的,他的底层实现就是将这个操做委托给了Netty

框架角度的理解

框架层面的理解有助于咱们在写代码中不会用错。有没有想过一个异步操做框架给你作了什么?
当你发起一个操做的时候,框架会去执行你的逻辑,在执行完毕时(成功或异常)去修改状态并执行你的回调修改状态并执行你的回调这个操做在JDK中放在了CompletableFuture中,在Netty中则单独采用了Promise接口,其实二者的实现是很是相似的(方法名都取的差很少)。以Netty举例分为Future和Promise两个方法,做为用户咱们更应该关心Future的接口,Promise是框架层面须要实现的,咱们在本身去实现的时候值得咱们去学习里面的思想。

不过我认为咱们直接使用Promise的这种接口的机会不多,Netty和VertX场景下仍是有机会用到,在用到Promise接口的时候应该考虑下是否合理,检查下是否是在同一个线程中,是否是能够简单的接口代替。给一个简单的错误示例:

这里说下Promise,咱们知道Js中也有一个Promise,千万不要当成相似的东西,二者毫无干系,Netty的Promise是对完成操做的行为的建模,Js的Promise是为了组合各个异步的调用。

import io.VertX.core.Future;

public class AuctionHandler {

  public Future<Void> handle() {
    // 请求级别变量
    Context context = new Context();
    context.future.tryComplete();
    return context.future;
  }

  public static class Context {
    Future<Void> future = Future.future();
  }
  public static void main(String[] args) { 
    // 注意这里的handle方法返回的Future是VertX的。
    // 这里的方法都是在同一个线程中执行的,彻底没有异步化,因此能够改为传递一个普通的接口便可
    new AuctionHandler().handle().setHandler(event1 -> System.out.println("handler exec!"));
  }
}
复制代码

虽然这个的代码错误看上去很低级,可是在开发VertX应用时须要时刻保持警戒。另外还有一点须要说明:当返回给你的Future已是完成状态时,如上面的代码示例,你再增长回调,这个回调还会被执行,Netty和CompletableFuture在添加回调的时候都是检查状态是否完成,完成的话直接投递到相应线程执行。

3、为何使用异步

为何要使用异步,相信不少同窗都知道是为了高性能,那么异步为何高性能?

这里先谈谈NodeJs和Java,对于NodeJs,不少人据说性能十分高,"秒杀"Java。我当时一直没法理解,为何Js能超过Java, 首先Node是单线程的,虽然能够借助第三方库来实现多线程,另外Jvm做为业界最优秀的虚拟机,那么Node究竟是靠了什么超过了Java?这里的关键就在于Node的Io模型采用了Reactor模型,能够处理大量的链接。Java中的Web开发是以Servlet为主导,采用了同步阻塞模型,虽然用线程池实现n个链接用m和线程作优化,可是当有大量链接时,线程数量过多致使的线程调度成本会很高,另外在线程处理Io的时候也是同步阻塞,若是对方返回很难会致使当前线程一直没法释放,因此Tomcat这种不适合处理大量链接的场景。

咱们知道Jetty的底层实现就是Reactor模型,Tomcat在8以后默认也用了Reactor是否是会大幅提升性能?不幸的是,虽然能够提升一些性能可是仍是没法和Node一较高低,他解决的是Http链接那一块的阻塞问题,可是因为Servlet的编程模型,大量的同步阻塞操做仍是没法避免,好比你在一个请求中去访问了数据库,这个线程就会一直被占用,必定程度上你能够经过增长线程来缓解可是线程过多又会增长调度的成本,可能会致使虚拟机假死。因此若是你的处理中有这种耗时操做,那他就是你的瓶颈,你的qps的上限就很低。在高并发场景下,Servlet的瓶颈会十分突出,只能经过大量的堆机器来水平扩展,可是没有很好的榨干服务器的性能。

因此咱们须要的是编程模型的改变,像Nodejs那样在同步阻塞的地方进行异步非阻塞或者异步阻塞化。Spring5.0中的 WebFlux给了一个对应的解决方案,提供了响应式编程的模型用以取代Servlet,他对常见同步阻塞的地方进行了重写,如Redis和Mysql等常见的IO。很早以前VertX(早期名字Node.X,Java版的Nodejs)框架也提供了这样的编程模型,对不少同步阻塞的地方进行了重写,这个框架十分轻量级,社区活跃度很是高,使用起来很是方便。这两个底层都是Netty,不得不说Netty实在是太强大了。也从另一个角度说明设计的重要性,语言反而是其次。NodeJs,WebFlux和VertX都采用了相似的Reactor模型,高性能服务器领域这个模型几乎已是最佳实践,理解这个模型就和多线程同样重要。我以为拿Servlet和NodeJs来作性能的对比,是十分不公平的。NodeJs在Java领悟的对手应该是VertX这种框架,关于高性能Web框架的对比,techempower这个网站已经给出了详细的排名,排名前十的大部分是Jvm语言,Nodejs在五十名以后了,因此不要在拿Servlet去和NodeJs作对比了,Servlet这种模型在高并发领悟必定会被逐渐取代。因此要深刻理解响应式编程,拥抱响应式编程,现有的代码以及将来的开发均可以用响应式编程来作优化。

那么异步到底解决了什么问题?

上面举的例子只是简单说明了现有的异步非阻塞框架的性能优点。可是这个问题我也没法给出准确的解释,只是谈谈我本身的理解:

  1. 非阻塞很好理解,若是是阻塞的,那个当前的用户线程必定被hang住,直到数据写完或者读完(这个过程当中这个线程就是没用的,因此咱们须要开启大量的线程),若是非阻塞能够当即返回,继续处理其余任务。
  2. 异步的理解我用一个例子来讲名:Netty中发起一个写操做时当即返回了一个Future,用户能够提供一个监听器执行写操做完成后的逻辑。试想若是这里是同步非阻塞的,即调用Future的sync方法(不要在EventLoop中调用,致使死锁),那就会白白浪费一个线程,

程序运行过程当中始终是围绕着两个主题:IO、CPU。CPU和IO的速度差距十分大,异步和Reactor模型都是为了平衡这个差距,让CPU能充分利用起来,不要由于IO和其余同步操做致使线程Hang住,始终处于可运行的状态,可使用少许的线程充分利用CPU。

4、理解这些能在实际中的应用

不少人可能会疑问就算了把这些弄的明明白白到底有什么用?其实若是你很好的掌握了Reactor的编程模型,不少问题就能想明白了下面谈下本身理解的有用的地方:

  1. 若是用过Redis都了解他是单线程来处理用户的请求的,他实际就是采用了Reactor模型来处理请求,也就很好的理解了为何Redis单线程能保持很好的性能。知道了他的实现,在使用的过程当中就知道尽可能避免大对象的传输,由于是单线程处理,若是一个链接传输大对象那么别的链接的请求将不能被及时处理。还有Redis须要处理过时的键,它内部有定时任务去清理过时键,那么既然Redis是单线程的这个任务由谁去执行呢?仍是那个处理请求的EventLoop线程,EventLoop线程其实不光处理IO请求,还会处理一些任务和定时任务用来避免锁(具体能够参考Netty的网络模型)
  2. 明白NodeJs的高性能,我以为也是一个应用,在技术选型的过程当中不用人云亦云。Java也有拿得出手的框架:Netty
  3. 采用响应式框架编写代码。,在开发响应式代码中心中也能保持警戒本身所写的代码会不会致使EventLoop的阻塞(阻塞EventLoop是至关严重的问题)。若是阻塞最好是能经过异步的api实现业务逻辑,若是避免不了阻塞或者耗时操做,则须要把任务投递到另外的线程池中去处理,任何状况下都不要去阻塞EventLoop,像VertX框架中如操做Mysql,PostgreSql这种都已经有了异步的实现。响应式编程一种趋势,从如今开始拥抱它吧!
  4. 在学习Dubbo的时候他默认的Rpc协议Dubbo协议底层就是Netty,消费者和提供者之间是单一长链接,因此官网也指出他更适合小数据量大并发,由于单个链接的带宽上限在7MByte左右。若是要传输文件,能够采用Http,这样的带宽上限就是物理网卡的上限,Http能够开启多个链接。
  5. 上面四条说了Reactor结合异步的,其实Jdk8中的CompletableFuture是一个很是优秀的异步实现,咱们在须要异步化逻辑时(好比调用第三方接口)能够充分利用这个类,我曾经也写过一点关于这个类:异步编程下降延迟

最后还想说一句,Netty这个框架实在是太强大了,线程模型设计十分优秀,VertX把不少异步操做委托给了底层的Netty,由于Netty实现中的EventLoop具备自然的线程隔离(一个EventLoop对应一个线程,只会被这个线程调用),不少地方免去了同步,VertX一样继承了这个优势,有机会必定好好看看Netty的设计和源码。

6、困惑

阻塞和同步的四种组合,对于异步阻塞仍是没法理解,这种模式真的存在吗?

参考文章

  1. 回调地狱的此生前世
  2. 怎样理解阻塞非阻塞与同步异步的区别? - 严肃的回答 - 知乎
  3. 怎样理解阻塞非阻塞与同步异步的区别? - 陈硕的回答 - 知乎
  4. Netty官网
  5. IO - 同步,异步,阻塞,非阻塞
  6. nodejs真的是单线程吗?
  7. 做为一个服务器,node.js 是性能最高的吗? - 圆胖肿的回答 - 知乎
  8. web框架性能排名
  9. Java8实战第11章
  10. Java并发编程实战
  11. Netty权威指南第二版
相关文章
相关标签/搜索