本文由得物技术团队 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 的原理之前,咱们先理清一下“异步”到底是怎么的一个概念。说起异步编程,在平时的开发还是比拟常见的。
例如以下的代码示例:@Asyncpublicvoidcreate() { //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)