1.前言

对于Java BIO、NIO、AIO的区别和原理,这样的文章十分的多的,但次要还是在BIO和NIO这两者之间探讨,而对于AIO这样的文章就少之又少了,很多只是介绍了一下概念和代码示例。

在理解AIO时,有留神到以下几个景象:

1、 2011年Java 7公布,外面减少了AIO称之为异步IO的编程模型,但曾经过来了近12年,平时应用的开发框架中间件,还是以NIO为主,例如网络框架Netty、Mina,Web容器Tomcat、Undertow。

2、 Java AIO又称为NIO 2.0,难道它也是基于NIO来实现的?

3、 Netty舍去了AIO的反对。https://github.com/netty/netty/issues/2515

4、 AIO看起来只是解决了有无,公布了个寂寞。
这几个景象未免会令很多人心存疑惑,所以决定写这篇文章时,不想简略的把AIO的概念再复述一遍,而是要透过景象, 如何剖析、思考和了解Java AIO的实质。

2.什么是异步

2.1 咱们所理解的异步

AIO的A是Asynchronous异步的意思,在理解AIO的原理之前,咱们先理清一下“异步”到底是怎么的一个概念。
说起异步编程,在平时的开发还是比拟常见,例如以下的代码示例:

@Asyncpublic void create() {    //TODO}public void build() {    executor.execute(() -> build());}

不论是用@Async注解,还是往线程池里提交工作,他们最终都是同一个后果,就是把要执行的工作,交给另外一个线程来执行。
这个时候,能够大抵的认为,所谓的“异步”,就是多线程,执行工作。

2.2 Java BIO和NIO到底是同步还是异步?

Java BIO和NIO到底是同步还是异步,咱们先依照异步这个思路,做异步编程。

2.2.1 BIO示例

byte [] data = new byte[1024];InputStream in = socket.getInputStream();in.read(data);// 接管到数据,异步解决executor.execute(() -> handle(data));public void handle(byte [] data) {    // TODO}

BIO在read()时,尽管线程阻塞了,但在收到数据时,能够异步启动一个线程去解决。

2.2.2 NIO示例

selector.select();Set<SelectionKey> keys = selector.selectedKeys();Iterator<SelectionKey> iterator = keys.iterator();while (iterator.hasNext()) {    SelectionKey key = iterator.next();    if (key.isReadable()) {        SocketChannel channel = (SocketChannel) key.channel();        ByteBuffer byteBuffer = (ByteBuffer) key.attachment();        executor.execute(() -> {            try {                channel.read(byteBuffer);                handle(byteBuffer);            } catch (Exception e) {            }        });    }}public static void handle(ByteBuffer buffer) {    // TODO}

同理,NIO尽管read()是非阻塞的,通过select()能够阻塞期待数据,在有数据可读的时候,异步启动一个线程,去读取数据和解决数据。

2.2.3 产生了解的偏差

此时咱们山盟海誓的说,Java的BIO和NIO是异步还是同步,取决你的情绪,你快乐给它个多线程,它就是异步的。

但果真如此么,在翻阅了大量博客文章之后,基本一致的说明了,BIO和NIO是同步的。

那问题点出在哪呢,是什么造成了咱们了解上的偏差呢?

那就是参考系的问题,以前学物理时,公交车上的乘客是静止还是静止,须要有参考系前提,如果以高空为参考,他是静止的,以公交车为参考,他是静止的。

Java IO也是一样,须要有个参考系,能力定义它是同步异步,既然咱们探讨的是IO是哪一种模式,那就是要针对IO读写操作这件事来了解,而其余的启动另外一个线程去解决数据,曾经是脱离IO读写的范畴了,不应该把他们扯进来。

2.2.4 尝试定义异步

所以以IO读写操作这事件作为参照,咱们先尝试的这样定义,就是发动IO读写的线程(调用read和write的线程),和实际操作IO读写的线程,如果是同一个线程,就称之为同步,否则是异步

  • 显然BIO只能是同步,调用in.read()以后线程阻塞,有数据返回的时候,接管到数据的还是原来的线程。
  • 而NIO也称之为同步,起因也是如此,调用channel.read()时,线程尽管不会阻塞,但读到数据的还是以后线程。

依照这个思路,AIO应该是发动IO读写的线程,和理论收到数据的线程,可能不是同一个线程
是不是这样呢,当初开始上Java AIO的代码。

2.3 Java AIO的程序示例

2.3.1 AIO服务端程序

public class AioServer {    public static void main(String[] args) throws IOException {        System.out.println(Thread.currentThread().getName() + " AioServer start");        AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open()                .bind(new InetSocketAddress("127.0.0.1", 8080));        serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {            @Override            public void completed(AsynchronousSocketChannel clientChannel, Void attachment) {                System.out.println(Thread.currentThread().getName() + " client is connected");                ByteBuffer buffer = ByteBuffer.allocate(1024);                clientChannel.read(buffer, buffer, new ClientHandler());            }            @Override            public void failed(Throwable exc, Void attachment) {                System.out.println("accept fail");            }        });        System.in.read();    }}public class ClientHandler implements CompletionHandler<Integer, ByteBuffer> {    @Override    public void completed(Integer result, ByteBuffer buffer) {        buffer.flip();        byte [] data = new byte[buffer.remaining()];        buffer.get(data);        System.out.println(Thread.currentThread().getName() + " received:"  + new String(data, StandardCharsets.UTF_8));    }    @Override    public void failed(Throwable exc, ByteBuffer buffer) {    }}

2.3.2 AIO客户端程序

public class AioClient {    public static void main(String[] args) throws Exception {        AsynchronousSocketChannel channel = AsynchronousSocketChannel.open();        channel.connect(new InetSocketAddress("127.0.0.1", 8080));        ByteBuffer buffer = ByteBuffer.allocate(1024);        buffer.put("Java AIO".getBytes(StandardCharsets.UTF_8));        buffer.flip();        Thread.sleep(1000L);        channel.write(buffer); }}

2.3.3 异步的定义猜测论断

别离运行服务端和客户端程序

在服务端运行后果里,

main线程发动serverChannel.accept的调用,增加了一个CompletionHandler监听回调,当有客户端连贯过去时,Thread-5线程执行了accep的completed回调办法。

紧接着Thread-5又发动了clientChannel.read调用,也增加了个CompletionHandler监听回调,当收到数据时,是Thread-1的执行了read的completed回调办法。

这个论断和下面异步猜测统一,发动IO操作(例如accept、read、write)调用的线程,和最终实现这个操作的线程不是同一个,咱们把这种IO模式称之AIO

当然了,这样定义AIO只是为了不便咱们了解,理论中对异步IO的定义可能更形象一点。

3.AIO示例引发思考的问题

1、 执行completed()办法的这个线程是谁创立的,什么时候创立的?

2、 AIO注册事件监听和执行回调是如何实现的?

3、 监听回调的实质是什么?

3.1 问题1:执行completed()办法的这个线程是谁创立的,什么时候创立的

个别,这样的问题,须要从程序的入口的开始理解,但跟线程相干,其实是能够从线程栈的运行状况来定位线程是怎么运行。

只运行AIO服务端程序,客户端不运行,打印一下线程栈(备注:程序在Linux平台上运行,其余平台略有差别)

剖析线程栈,发现,程序启动了那么几个线程

1、 线程Thread-0阻塞在EPoll.wait()办法上

2、 线程Thread-1、Thread-2。。。Thread-n(n和CPU外围数量统一)从阻塞队列里take()工作,阻塞期待有工作返回。

此时能够暂定下一个论断:

AIO服务端程序启动之后,就开始创立了这些线程,且线程都处于阻塞期待状态。

另外,发现这些线程的运行都跟Epoll有关系,提到Epoll,咱们印象中,Java NIO在Linux平台底层就是用Epoll来实现的,难道Java AIO也是用Epoll来实现么?为了证实这个论断,咱们从下一个问题来展开讨论

3.2 问题2:AIO注册事件监听和执行回调是如何实现的

带着这个问题,去浏览剖析源码时,发现源码特地的长,而源码解析是一项枯燥乏味的过程,很容易把阅读者给逼走劝退掉。

对于长流程和逻辑简单的代码的了解,咱们能够抓住它几个脉络,找出哪几个外围流程。

以注册监听read为例clientChannel.read(...),它次要的外围流程是:

1、注册事件 -> 2、监听事件 -> 3、处理事件

3.2.1 1、注册事件

注册事件调用EPoll.ctl(...)函数,这个函数在最初的参数用于指定是一次性的,还是永久性。下面代码events | EPOLLONSHOT字面意思看来,是一次性的。

3.2.2 2、监听事件

3.2.3 3、处理事件

3.2.4 外围流程总结

在剖析完下面的代码流程后会发现,每一次IO读写都要经验的这三个事件是一次性的,也就是在处理事件完,本次流程就完结了,如果想持续下一次的IO读写,就得从头开始再来一遍。这样就会存在所谓的死亡回调(回调办法里再增加下一个回调办法),这对于编程的复杂度大大提高了。

3.3 问题3: 监听回调的实质是什么?

先说一下论断,所谓监听回调的实质,就是用户态线程,调用内核态的函数(精确的说是API,例如read,write,epollWait),该函数还没有返回时,用户线程被阻塞了。当函数返回时,会唤醒阻塞的线程,执行所谓回调函数

对于这个论断的了解,要先引入几个概念

3.3.1 零碎调用与函数调用

函数调用:

找到某个函数,并执行函数里的相干命令

零碎调用:

操作系统对用户应用程序提供了编程接口,所谓API。

零碎调用执行过程:

1.传递零碎调用参数

2.执行陷入指令,用用户态切换到外围态,这是因为零碎调用个别都须要再外围态下执行

3.执行零碎调用程序

4.返回用户态

3.3.2 用户态和内核态之间的通信

用户态->内核态,通过零碎调用形式即可。

内核态->用户态,内核态基本不晓得用户态程序有什么函数,参数是啥,地址在哪里。所以内核是不可能去调用用户态的函数,只能通过发送信号,比方kill 命令关闭程序就是通过发信号让用户程序优雅退出的。

既然内核态是不可能被动去调用用户态的函数,为什么还会有回调呢,只能说这个所谓回调其实就是用户态的自导自演。它既做了监听,又做了执行回调函数。

3.3.3 用理论例子验证论断

为了验证这个论断是否有说服力,举个例子,平时开发写代码用的IntelliJ IDEA,它是如何监听鼠标、键盘事件和处理事件的。

依照常规,先打印一下线程栈,会发现鼠标、键盘等事件的监听是由"AWT-XAWT"线程负责的,处理事件则是"AWT-EventQueue"线程负责。

定位到具体的代码上,能够看到"AWT-XAWT"正在做while循环,调用waitForEvents函数期待事件返回。如果没有事件,线程就始终阻塞在那边。

4.Java AIO的实质是什么?

1、因为内核态无奈间接调用用户态函数,Java AIO的实质,就是只在用户态实现异步。并没有达到现实意义上的异步。

现实中的异步

何谓现实意义上的异步?这里举个网购的例子

两个角色,消费者A,快递员B

  • A在网上购物时,填好家庭地址付款提交订单,这个相当于注册监听事件
  • 商家发货,B把货色送到A家门口,这个相当于回调。

A在网上下完单,后续的发货流程就不必他来操心了,能够持续做其余事。B送货也不关怀A在不在家,反正就把货扔到家门口就行了,两个人互不依赖,互不相烦扰

假如A购物是用户态来做,B送快递是内核态来做,这种程序运行形式过于现实了,理论中实现不了。

事实中的异步

A住的是低档小区,不能随便进去,快递只能送到小区门口。

A买了一件比拟重的商品,比方一台电视,因为A要下班不在家里,所以找了一个好友C帮忙把电视搬到他家。
A出门下班前,跟门口的保安D打声招呼,说明天有一台电视送过来,送到小区门口时,请电话分割C,让他过去拿。

  • 此时,A下单并跟D打招呼,相当于注册事件。在AIO中就是EPoll.ctl(...)注册事件。
  • 保安在门口蹲着相当于监听事件,在AIO中就是Thread-0线程,做EPoll.wait(..)
  • 快递员把电视送到门口,相当于有IO事件达到。
  • 保安告诉C电视到了,C过去搬电视,相当于处理事件。

在AIO中就是Thread-0往工作队列提交工作,

Thread-1 ~n去取数据,并执行回调办法。

整个过程中,保安D必须始终蹲着,寸步不能来到,否则电视送到门口,就被人偷了。

好友C也必须在A家待着,受人委托,货色到了,人却不在现场,这有点失信于人。

所以理论的异步和现实中的异步,在互不依赖,互不烦扰,这两点相违反了。保安的作用最大,这是他人生的高光时刻。

异步过程中的注册事件、监听事件、处理事件,还有开启多线程,这些过程的发起者全是用户态一手操办,所以说Java AIO只在用户态实现了异步,这个和BIO、NIO先阻塞,阻塞唤醒后开启异步线程解决的实质统一。

2、Java AIO跟NIO一样,在各个平台的底层实现形式也不同,在Linux是用EPoll,Windows是IOCP,Mac OS是KQueue。原理是大同小异,都是须要一个用户线程阻塞期待IO事件,一个线程池从队列里处理事件。

3、 Netty之所以移除掉AIO,很大的起因是在性能上AIO并没有比NIO高。Linux尽管也有一套原生的AIO实现(相似Windows上的IOCP),但Java AIO在Linux并没有采纳,而是用EPoll来实现。

4、 Java AIO不反对UDP

5、 AIO编程形式略显简单,比方“死亡回调”