I/O 的实际操作由内核执行,其中一个重要手段是缓冲区。简单来说 I/O 可分为两类:面向磁盘和面向网络,Java 也是针对这两者来抽象设计 API,相关的类主要在 java.io
和 java.nio
包中,简称为 BIO 和 NIO。
为什么设计 NIO
一个直接原因就是为了更好的利用操作系统特性,改善和扩展原有 API。与 NIO 相关的规范有两个:
- JSR 51:它是 NIO 的第一个规范,关注缓冲区、通道和字符集的设计,引入一个简单的面向缓冲区的 I/O 模型,并且提供一套非阻塞、I/O 多路复用、可扩展的 API;
- JSR 203(NIO.2):它在前者的基础上,添加新的文件系统的抽象,完善现有 Socket 通道的配置,添加多播数据报的支持,并且定义了一个异步 I/O 编程 API。
那么,传统的 BIO 又有什么弊端?NIO 又是如何改进的?
文件操作
关于 java.io.file
,它的不足之处在于:
- 当查询文件属性时,如修改时间或文件类型,都会发生系统调用,并且这些组合操作非常常见,造成性能问题;
- 部分方法在发生错误时返回 false 而不是抛出异常,比如 delete、rename;
- 一些 OS 常用功能不支持,比如符号链接、文件锁定、内存映射等。
而 NIO 支持批量获取文件属性,对文件、目录的处理也重新设计,提供 FileLock、MappedByteBuffer。
网络通信
传统 BIO 是阻塞式、基于流的I/O,其网络服务器模型是一连接一线程,通常采用线程池优化,但一个进程或者计算机打开的线程数是有限的,可扩展性差。
NIO 是基于缓冲区(也是由字节流或字符流组成)的,对原始 I/O 提供了新的抽象 - Channel(通道)。Channel
表示一个到硬件设备、文件或网络套接字的连接,与 java.net.Socket 的区别是:
- 可配置非阻塞,允许事件驱动的设计,提供了一种更加可扩展的服务器开发;
- 面向字节缓冲区,可实现 零拷贝 执行 I/O ,一端得是 FileChannel。
NIO 主要目标是设计、开发可扩展/可伸缩性服务器,让少量的线程管理大量的客户端连接,而灵活的代价是编程复杂,一复杂就会有人抽象出框架,NIO 常用的框架是 Netty,这里想到一个问题,Netty 宣称的零拷贝与OS级别的有区别吗?
服务器常用的优化手段是对象池、减少数据复制(内核到用户进程或用户进程内部)、减少上下文切换和锁,ByteBuffer 本质就是提供了一个可复用的 byte[] 数组,而 BIO 做好这些优化也不见得比 NIO 慢,那么如何选择I/O模型?
I/O 模型的选择
首先了解一下 C10k problem - 描述单机处理1万个并发连接的问题,两个不同的概念:
- 并发连接(concurrent connections):在有限的时间内响应请求,关注高效的连接调度;
- 每秒请求数(requests per second):快速处理请求以响应,关注高吞吐量。
C10k 问题的本质在于 CPU,即线程数,单机创建大量线程,不仅占用大量的内存,频繁的数据复制和上下文切换还会导致 OS 崩溃。
如果采用 BIO 一个直观的解决办法是水平扩展,采用分布式系统,但如果并发量上升到百万、千万、甚至上亿,那么服务器的成本得多大?解决此问题的关键是减少线程数,提高单机的处理能力,而如何使用少量的线程管理大量的连接,则在操作系统层面解决了,也就是 Linux 下的 Epoll,Java 中的 NIO。如果你的应用面临 C10k 问题,NIO 是最好的选择。
那 BIO 有什么用呢?大家都用线程池,你有 ByteBuffer,我也可以自己维护字节缓冲,照样成块读取,阻塞无非因为 I/O 延迟高,那换成 SSD和光纤,而且我编程简单,唯一的缺点就是扩展性差了点。:)
至于如何选择 I/O 模型,需要结合业务场景,综合考虑:
- 短连接还是长连接
- 预计最大的并发数
- 预计每个连接的数据量,即流量的大小
但感觉还是很难给出明确的答案,简单来说,并发量低的可采用 BIO,高的可采用 NIO,至于 AIO 它应该不太成熟,不过多描述了。
小结
写的可能有点乱,等以后有更多的体会,再优化吧。欢迎讨论。
本文由 wskwbog 创作,采用 知识共享4.0 许可证 - 署名-非商业性使用-禁止演绎
本站文章除注明转载/出处外,均为本站原创或翻译