作者: 公众号: 我是攻城师
前言
Java 里面的 IO 模型种类较多, 主要包括 BIO,NIO 和 AIO, 每个 IO 模型都有不一样的地方, 那么这些 IO 模型是如何演变呢, 底层的原理又是怎样的呢? 本文我们就来聊聊.
BIO
BIO 全称是 Blocking IO, 是 JDK1.4 之前的传统 IO 模型, 本身是同步阻塞模式, 针对网络通信都是一请求一应答的方式, 虽然简化了上层的应用开发, 但在性能和可靠性方面存在着巨大瓶颈, 试想一下如果每个请求都需要新建一个线程来专门处理, 那么在高并发的场景下, 机器资源很快就会被耗尽, 当然, 我们可以通过线程池来优化这种情况, 但即使是这样, 仍然改变不了阻塞 IO 的根本问题, 就是在 IO 执行的两个阶段都被 block 了. 拿一个 read 操作来举例子, 在 Linux 中, 应用程序向 Linux 发起 read 操作, 会经历两个步骤:
第一个阶段 Linux 内核首先会把需要读取的数据加载到操作系统内核的缓冲区中 (Linux 文件系统是缓存 IO, 也称标准 IO)
第二个阶段应用程序拷贝内核里面的数据到自己的用户空间中
如果是 socket 操作, 类似也会经历两个步骤:
第一个阶段: 通常涉及等待网络上的数据分组包到达, 然后被复制到内核的缓冲区
第二个阶段: 把数据从内核缓冲区, 从内核缓冲区拷贝到用户进程的内存空间里面
同步阻塞 IO 之所以效率低下, 就是因为在这两个阶段, 用户的线程或者进程都是阻塞的, 期间虽然不占 CPU 资源, 但也意味着该线程也不能再干其他事. 有点站着茅坑不拉屎的感觉, 自己暂时不用了, 也不让别人用.
图示如下:
NIO
由于 BIO 的缺点, 导致 Java 在 JDK1.0 至 JDK3.0 中, 网络通信模块的性能一直是短板, 所以很多人更倾向于使用 C/C++ 开发高性能服务端. 为了强化 Java 在服务端的市场, 终于在 JSR-51 也就是 JDK4.0 的时候发布了 Java NIO, 可以支持非阻塞 IO. 并新增了 java.nio 的包, 提供很多异步开发的 API 和类库.
主要的类和接口如下:
(1) 进行异步 IO 操作的缓冲区 ByteBuffer
(2) 进行异步 IO 操作的管道 Pipe
(3) 进行各种 IO 操作的 Channel, 主要包括 ServerSocketChannel 和 SocketChannel
(4) 实现非阻塞 IO 的多路复用器 Selector
NIO 主要有 buffer,channel,selector 三种技术的整合, 通过零拷贝的 buffer 取得数据, 每一个客户端通过 channel 在 selector(多路复用器) 上进行注册. 服务端不断轮询 channel 来获取客户端的信息. channel 上有 connect,accept(阻塞),read(可读),write(可写) 四种状态标识. 根据标识来进行后续操作. 所以一个服务端可接收无限多的 channel. 不需要新开一个线程. 大大提升了性能.
新的 nio 类库, 促进了异步非阻塞编程的发展和应用, 但仍然有一些不足之处:
(1) 没有统一的文件属性, 例如读写权限
(2)API 能力比较弱, 例如目录的及联创建和递归遍历, 往往需要自己完成.
(3) 底层操作系统的一些高级 API 无法使用
(4) 所有的文件操作都是同步阻塞调用, 在操作系统层面上并不是异步文件读写操作.
Java 里面的 NIO 其实采用了多路复用的 IO 模式, 多路复用的模式在 Linux 底层其实是采用了 select,poll,epoll 的机制, 这种机制可以用单个线程同时监听多个 io 端口, 当其中任何一个 socket 的数据准备好了, 就能返回通知用户线程进行读取操作, 与阻塞 IO 阻塞的是每一个用户的线程不一样的地方是, 多路复用只需要阻塞一个用户线程即可, 这个用户线程通常我们叫它 Selector, 其实底层调用的是内核的 select, 这里面只要任何一个 IO 操作就绪, 就可以唤醒 select, 然后交由用户线程处理. 用户线程读取数据这个过程仍然是阻塞的, 多路复用技术只是在第一个阶段可以变为非阻塞调用, 但在第二个阶段拷贝数据到用户空间, 其实还是阻塞的, 多路复用技术的最大特点是使用一个线程就可以处理很多的 socket 连接, 尽管性能上不一定提升, 但支持并发能力却大大增强了.
图示如下:
AIO
AIO, 其实是 NIO 的改进优化, 也被称为 NIO2.0, 在 2011 年 7 月, 也就是 JDK7 的版本中发布, 它主要提供了三个方面的改进:
(1) 提供了能够批量获取文件属性的 API, 通过 SPI 服务, 使得这些 API 具有平台无关性.
(2) 提供了 AIO 的功能, 支持基于文件的异步 IO 操作和网络套接字的异步操作
(3) 完成了 JSR-51 定义的通道功能等.
AIO 通过调用 accept 方法, 一个会话接入之后再次调用 (递归)accept 方法, 监听下一次会话, 读取也不再阻塞, 回调 complete 方法异步进行. 不再需要 selector 使用 channel 线程组来接收.
从 NIO 上面我们能看到, 对于 IO 的两个阶段的阻塞, 只是对于第一个阶段有所改善, 对于第二个阶段在 NIO 里面仍然是阻塞的. 而真正的理想的异步非阻塞 IO(AAIO) 要做的就是, 将 IO 操作的两个阶段都全部交给内核系统完成, 用户线程只需要告诉内核, 我要读取一块数据, 请你帮我读取, 读取完了放在我给你的地址里面, 然后告诉我一声就可以了.
AIO 可以做到真正的异步的操作, 但实现起来比较复杂, 支持纯异步 IO 的操作系统非常少, 目前也就 Windows 是 IOCP 技术实现了, 而在 Linux 上, 目前有很多开源的异步 IO 库, 例如 libevent,libev,libuv, 但基本都不是纯的异步 IO 操作, 底层还是是使用的 epoll 实现的.
图示如下:
NIO 与 Netty
既然 Java 拥有了各种 IO 体系, 那么为什么还会出现 Netty 这种框架呢?
Netty 出现的主要原因, 如下:
(1)Java NIO 类库和 API 繁杂众多, 使用麻烦.
(2)Java NIO 封装程度并不高, 常常需要配合 Java 多线程编程来使用, 这是因为 NIO 编程涉及到 Reactor 模式.
(3)Java NIO 异常体系不完善, 如客户端面临断连, 重连, 网络闪断, 半包读写, 网络阻塞, 异常码流等问题, 虽然开发相对容易, 但是可靠性和稳定性并不高.
(4)Java NIO 本身的 bug, 修复较慢.
注意, 真正的异步非阻塞 io, 是需要操作系统层面支持的, 在 Windows 上通过 IOCP 实现了真正的异步 io, 所以 Java 的 AIO 的异步在 Windows 平台才算真正得到了支持, 而在 Linux 系统中, 仍然用的是 epoll 模式, 所以在 Linux 层面上的 AIO, 并不是真正的或者纯的异步 IO, 这也是 Netty 里面为什么采用 Java 的 NIO 实现的, 而并非是 AIO, 主要原因如下:
(1)AIO 在 Linux 上底层实现仍使用 EPOLL, 与 NIO 相同, 因此在性能上没有明显的优势
(2)Windows 的 AIO 底层实现良好, 但 Netty 的开发者并没有把 Windows 作为主要使用平台, 所以优化考虑 Linux
总结
本文主要介绍了 Java 里面 IO 模型的演变和发展, 这也是 Java 在服务端领域大放异彩的一个重要原因, 了解这些知识之后, 我们再去学习高性能的 Netty 框架, 将会更加容易.
来源: https://www.cnblogs.com/yuxiang1/p/10003866.html