写点什么

到底什么是 Java AIO?为什么 Netty 会移除 AOI?一文搞懂 AIO 的本质!

作者:JackJiang
  • 2023-06-21
    江苏
  • 本文字数:5994 字

    阅读完需:约 20 分钟

到底什么是Java AIO?为什么Netty会移除AOI?一文搞懂AIO的本质!

本文由得物技术团队 Uni 分享,即时通讯网收录时有内容修订和大量排版优化。


1、引言关于 Java 网络编程中的同步 IO 和异步 IO 的区别及原理的文章非常的多,具体来说主要还是在讨论 Java BIO 和 Java NIO 这两者,而关于 Java 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 的支持?(点此查看);4)AIO 看起来貌似只是解决了有无,实际是发布了个寂寞?Java AIO 的这些不合常理的现象难免会令人心存疑惑。所以决定写这篇文章时,我不想只是简单的把 AIO 的概念再复述一遍,而是要透过现象,深入分析、思考和并理解 Java AIO 的本质。


技术交流:


  • 移动端 IM 开发入门文章:《新手入门一篇就够:从零开发移动端 IM》

  • 开源 IM 框架源码:https://github.com/JackJiang2011/MobileIMSDK(备用地址点此)


(本文已同步发布于:http://www.52im.net/thread-4283-1-1.html


2、我们所理解的异步 AIO 的 A 是 Asynchronous(即异步)的意思,在了解 AIO 的原理之前,我们先理清一下“异步”到底是怎样的一个概念。


说起异步编程,在平时的开发还是比较常见的。


例如以下的代码示例:


@Async


publicvoidcreate() {


//TODO
复制代码


}


publicvoidbuild() {


executor.execute(() -> build());
复制代码


}


不管是用 @Async 注解,还是往线程池里提交任务,他们最终都是同一个结果,就是把要执行的任务,交给另外一个线程来执行。


这个时候,我们可以大致的认为,所谓的“异步”,就是用多线程的方式去并行执行任务。


3、Java BIO 和 NIO 到底是同步还是异步?Java BIO 和 NIO 到底是同步还是异步,我们先按照异步这个思路,做异步编程。


3.1BIO 代码示例 byte[] data = newbyte[1024];


InputStream in = socket.getInputStream();


in.read(data);


// 接收到数据,异步处理


executor.execute(() -> handle(data));


publicvoidhandle(byte[] data) {


// TODO
复制代码


}


如上:BIO 在 read()时,虽然线程阻塞了,但在收到数据时,可以异步启动一个线程去处理。


3.2NIO 代码示例 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) {


}
});
}
复制代码


}


publicstaticvoidhandle(ByteBuffer buffer) {


// TODO
复制代码


}


同理:NIO 虽然 read()是非阻塞的,通过 select()可以阻塞等待数据,在有数据可读的时候,异步启动一个线程,去读取数据和处理数据。


3.3 产生的理解偏差此时我们信誓旦旦地说,Java 的 BIO 和 NIO 是异步还是同步,取决你的心情,你高兴给它个多线程,它就是异步的。


但果真如此么?


在翻阅了大量博客文章之后,基本一致的阐明了——BIO 和 NIO 是同步的。


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


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


Java IO 也是一样,需要有个参考系,才能定义它是同步还是异步。


既然我们讨论的是关于 Java IO 是哪一种模式,那就是要针对 IO 读写操作这件事来理解,而其他的启动另外一个线程去处理数据,已经是脱离 IO 读写的范围了,不应该把他们扯进来。


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


按上述定义:


1)显然 BIO 只能是同步,调用 in.read()当前线程阻塞,有数据返回的时候,接收到数据的还是原来的线程;2)而 NIO 也称之为同步,原因也是如此,调用 channel.read()时,线程虽然不会阻塞,但读到数据的还是当前线程。按照这个思路,AIO 应该是发起 IO 读写的线程,和实际收到数据的线程,可能不是同一个线程。


是不是这样呢?我们将在上一节直接上 Java AIO 的代码,我们从 实际代码中一窥究竟吧。


4、一个 Java AIO 的网络编程示例 4.1AIO 服务端程序代码 publicclassAioServer {


publicstaticvoidmain(String[] args) throwsIOException {
System.out.println(Thread.currentThread().getName() + " AioServer start");
AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open()
.bind(newInetSocketAddress("127.0.0.1", 8080));
serverChannel.accept(null, newCompletionHandler<AsynchronousSocketChannel, Void>() {


@Override
publicvoidcompleted(AsynchronousSocketChannel clientChannel, Void attachment) {
System.out.println(Thread.currentThread().getName() + " client is connected");
ByteBuffer buffer = ByteBuffer.allocate(1024);
clientChannel.read(buffer, buffer, newClientHandler());
}


@Override
publicvoidfailed(Throwable exc, Void attachment) {
System.out.println("accept fail");
}
});
System.in.read();
}
复制代码


}


publicclassClientHandler implementsCompletionHandler<Integer, ByteBuffer> {


@Override
publicvoidcompleted(Integer result, ByteBuffer buffer) {
buffer.flip();
byte[] data = newbyte[buffer.remaining()];
buffer.get(data);
System.out.println(Thread.currentThread().getName() + " received:"+ newString(data, StandardCharsets.UTF_8));
}


@Override
publicvoidfailed(Throwable exc, ByteBuffer buffer) {


}
复制代码


}


4.2AIO 客户端程序 publicclassAioClient {


publicstaticvoidmain(String[] args) throwsException {
AsynchronousSocketChannel channel = AsynchronousSocketChannel.open();
channel.connect(newInetSocketAddress("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);
复制代码


}


}


4.3 异步的定义猜想结论分别运行服务端和客户端程序:


在服务端运行结果里:


1)main 线程发起 serverChannel.accept 的调用,添加了一个 CompletionHandler 监听回调,当有客户端连接过来时,Thread-5 线程执行了 accep 的 completed 回调方法。


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


这个结论和上面异步猜想一致:发起 IO 操作(例如 accept、read、write)调用的线程,和最终完成这个操作的线程不是同一个,我们把这种 IO 模式称之 AIO。


当然了,这样定义 AIO 只是为了方便我们理解,实际中对异步 IO 的定义可能更抽象一点。


5、 AIO 示例引发思考 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 来实现么?


为了证实这个结论,我们从下一个问题来展开讨论。


6、 AIO 示例引发思考 2:AIO 注册事件监听和执行回调是如何实现的?带着这个问题,去阅读 JDK 分析源码时,发现源码特别的长,而源码解析是一项枯燥乏味的过程,很容易把阅读者给逼走劝退掉。


对于长流程和逻辑复杂的代码的理解,我们可以抓住它几个脉络,找出哪几个核心流程。


以注册监听 read 为例 clientChannel.read(...),它主要的核心流程是:注册事件 -> 监听事件 -> 处理事件。


注册事件:


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


监听事件:


处理事件:


核心流程总结:


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


7、 AIO 示例引发思考 3:监听回调的本质是什么?7.1 概述先说一下结论:所谓监听回调的本质,就是用户态线程调用内核态的函数(准确的说是 API,例如 read、write、epollWait),该函数还没有返回时,用户线程被阻塞了。当函数返回时,会唤醒阻塞的线程,执行所谓回调函数。


对于这个结论的理解,要先引入几个概念。


7.2 系统调用与函数调用函数调用:找到某个函数,并执行函数里的相关命令。


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


系统调用执行过程:


1)传递系统调用参数;2)执行陷入指令,用用户态切换到核心态(这是因为系统调用一般都需要再核心态下执行);3)执行系统调用程序;4)返回用户态。7.3 用户态和内核态之间的通信用户态->内核态:通过系统调用方式即可。


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


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


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


按照惯例,先打印一下线程栈,会发现鼠标、键盘等事件的监听是由“AWT-XAWT”线程负责的,处理事件则是“AWT-EventQueue”线程负责。如下图所示。


定位到具体的代码上:可以看到“AWT-XAWT”正在做 while 循环,调用 waitForEvents 函数等待事件返回。如果没有事件,线程就一直阻塞在那边。如下图所示。


8、Java AIO 的本质是什么?8.1Java AIO 的本质,就是只在用户态实现了异步由于内核态无法直接调用用户态函数,Java AIO 的本质,就是只在用户态实现异步,并没有达到理想意义上的异步。


1)理想中的异步:


何谓理想意义上的异步?这里举个网购的例子。


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


1)A 在网上购物时,填好家庭地址付款提交订单,这个相当于注册监听事件;2)商家发货,B 把东西送到 A 家门口,这个相当于回调。A 在网上下完单,后续的发货流程就不用他来操心了,可以继续做其他事。B 送货也不关心 A 在不在家,反正就把货扔到家门口就行了,两个人互不依赖,互不相干扰。


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


2)现实中的异步:


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


A 买了一件比较重的商品,比如一台电视,因为 A 要上班不在家里,所以找了一个好友 C 帮忙把电视搬到他家。


A 出门上班前,跟门口的保安 D 打声招呼,说今天有一台电视送过来,送到小区门口时,请电话联系 C,让他过来拿。


具体就是:


1)此时,A 下单并跟 D 打招呼,相当于注册事件。在 AIO 中就是 EPoll.ctl(...)注册事件;2)保安在门口蹲着相当于监听事件,在 AIO 中就是 Thread-0 线程,做 EPoll.wait(..);3)快递员把电视送到门口,相当于有 IO 事件到达;4)保安通知 C 电视到了,C 过来搬电视,相当于处理事件(在 AIO 中就是 Thread-0 往任务队列提交任务,Thread-1 ~n 去取数据,并执行回调方法)。整个过程中,保安 D 必须一直蹲着,寸步不能离开,否则电视送到门口,就被人偷了。


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


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


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


8.2Java AIO 的其它真相 Java AIO 跟 NIO 一样:在各个平台的底层实现方式也不同,在 Linux 是用 epoll、Windows 是 IOCP、Mac OS 是 KQueue。原理是大同小异,都是需要一个用户线程阻塞等待 IO 事件,一个线程池从队列里处理事件。


Netty 之所以移除掉 AIO:很大的原因是在性能上 AIO 并没有比 NIO 高。Linux 虽然也有一套原生的 AIO 实现(类似 Windows 上的 IOCP),但 Java AIO 在 Linux 并没有采用,而是用 epoll 来实现。


Java AIO 不支持 UDP。


AIO 编程方式略显复杂,比如“死亡回调”。


9、参考资料[1] 少啰嗦!一分钟带你读懂 Java 的 NIO 和经典 IO 的区别


[2] 史上最强 Java NIO 入门:担心从入门到放弃的,请读这篇!


[3] Java 的 BIO 和 NIO 很难懂?用代码实践给你看,再不懂我转行!


[4] Java 新一代网络编程模型 AIO 原理及 Linux 系统 AIO 介绍


[5] 从 0 到 1 的快速裂变:详解快的打车架构设计及技术实践


[6] 新手入门:目前为止最透彻的的 Netty 高性能原理和框架架构解析


[7] 史上最通俗 Netty 框架入门长文:基本介绍、环境搭建、动手实战


[8] 高性能网络编程(五):一文读懂高性能网络编程中的 I/O 模型


[9] 高性能网络编程(六):一文读懂高性能网络编程中的线程模型


[10] 高性能网络编程(七):到底什么是高并发?一文即懂!


[11] 从根上理解高性能、高并发(二):深入操作系统,理解 I/O 与零拷贝技术


[12] 从根上理解高性能、高并发(三):深入操作系统,彻底理解 I/O 多路复用


[13] 从根上理解高性能、高并发(四):深入操作系统,彻底理解同步与异步


[14] 从根上理解高性能、高并发(五):深入操作系统,理解高并发中的协程


(本文已同步发布于:http://www.52im.net/thread-4283-1-1.html

用户头像

JackJiang

关注

还未添加个人签名 2019-08-26 加入

开源IM框架MobileIMSDK、BeautyEye的作者。

评论

发布
暂无评论
到底什么是Java AIO?为什么Netty会移除AOI?一文搞懂AIO的本质!_网络编程_JackJiang_InfoQ写作社区