IO 和 NIO 的思考


输入输出是操作系统不可或缺的一部分,大致分为两类:面向磁盘和面向网络。在 Java 中有3种 I/O 类型:BIO、NIO 和 AIO,分别是同步阻塞、同步非阻塞和异步非阻塞 I/O,这里着重描述 BIO 和 NIO 的区别和常用的编程模型。

1. 为什么设计 NIO

一个直接原因就是为了更好的利用操作系统特性,改善和扩展原有 API。与 NIO 相关的规范有两个:

  • JSR 51:它是 NIO 的第一个规范,关注缓冲区、通道和字符集的设计,引入一个简单的面向缓冲区的 I/O 模型,并且提供一套非阻塞、I/O 多路复用、可扩展的 API
  • JSR 203(NIO.2):它在前者的基础上,添加新的文件系统的抽象,完善现有 Socket 通道的配置,添加多播数据报的支持,并且定义了一个异步 I/O 编程 API

那么,传统的 BIO 又有什么弊端?NIO 又是如何改进的?可以从两方面进行说明。

1.2 文件操作

关于 java.io.file,它的不足之处在于:

  • 查询文件属性时,如修改时间或文件类型,都会发生系统调用,并且这些组合操作非常常见,造成性能问题
  • 部分方法在发生错误时返回 false 而不是抛出异常,比如 delete、rename,不知操作失败的原因
  • 一些 OS 高级功能不支持,比如符号链接、文件锁定、内存映射等

而 NIO 支持批量获取文件属性,对文件、目录的处理也重新设计,提供 FileLock、MappedByteBuffer 等支持 OS 高级功能。

1.3 网络通信

BIO 是同步阻塞、基于流的 I/O,阻塞就意味着当 Socket 输入流中无数据可读取时,调用线程挂起,直到有数据读取,期间不能处理其他请求,如果来了一个新连接,就只能再新建一个线程处理。

随着连接数的增加,BIO 将会创建大量线程,而一个计算机能打开的进程数或线程数是有限的,严重的时候可能会导致应用崩溃无响应。一个有效的解决办法是使用线程池,限制最大线程数,但它同时也限制了最大连接数。

NIO 将读写改为非阻塞,无数据可读,线程返回线程池,可用于处理其他连接。它对原始 I/O 提供了新的抽象 - Channel(通道),并且提供基于缓冲区的读写 API。Channel 表示一个到硬件设备、文件或网络套接字的连接,与 java.net.Socket 的区别是:

  • 可配置非阻塞,允许事件驱动的设计,提供了一种更加可扩展的服务器开发
  • 面向缓冲区,可实现零拷贝执行 I/O ,只不过有一端必须是 FileChannel

相同环境下,BIO 的线程全程只处理一条连接,而 NIO 的线程可处理多个连接,提高了系统的吞吐能力。NIO 在服务器进行纵向扩展(比如增加内存、CPU)或者横向扩展(比如增加服务器)往往能够比 BIO 带来更高的处理能力,使服务器具有更强的可扩展性和可伸缩性。

1.4 零拷贝

NIO 还有一个零拷贝的概念,零拷贝是指 CPU 不执行将数据从一个存储区复制到另一个存储区的操作。OS 级别的零拷贝指的是将数据发送到硬件驱动程序(网卡或磁盘驱动器)时避免从一个位置复制到另一个位置(一般是从用户空间到内核空间),反之亦然。NIO 中的零拷贝就是这样,只不过它只针对在网络上发送文件。

2. I/O 模型的选择

一般的,我们潜意识的会认为 NIO 比 BIO 的性能高,其实不尽然,当然了有个读取方式的问题,read(byte[]) 和 read(ByteBuffer)应该没区别吧?所以如果系统的并发量不高,两个用谁都行。

BIO 的问题通常会在海量的连接下体现出来,由于它不能充分利用、压榨一台服务器的性能,不管怎么扩展,它能处理的连接数与机器性能往往是非线性的,付出和收获不成正比。如果你的应用面临的连接不断增加,特别是存在大量的长连接,此时就要选择 NIO,它不仅提高了单机处理能力,还能节省服务器成本。

NIO 相比 BIO 的重点在于可扩展性,在选择 I/O 模型时,需要结合业务场景,综合考虑以下几点:

  • 预计最大的并发数
  • 短连接还是长连接
  • 预计每个连接的数据量,即流量的大小
  • NIO 灵活,但代价是编程复杂

3. 编程模型

BIO 的编程模型是一连接一处理线程,采用线程池优化。

NIO 典型的编程模型是 Reactor,事件复用器通知套接字何时准备好读取和写入操作的事件,将事件传递给合适的处理程序,由该程序负责实际的读取或写入。对于读操作基本过程如下:

  • 处理程序声明感兴趣的 I/O 事件 - 读取事件
  • 事件复用器等待事件
  • 一个事件发生,复用器被唤醒并调用适当的处理程序
  • 处理程序执行实际的读取操作,处理读取的数据,重新声明关注的 I/O 事件,并将控制权返回给调度程序

与 Reactor 相对的还有一个 AIO 的 Proactor 模型,它是异步 I/O,事件复用器等待 I/O 操作完成的事件,它是真正的异步,因为实际的 I/O 操作完全由操作系统执行。对于读操作,它的做法是:

  • 处理程序启动异步读取操作,在这种情况下,处理程序不关心 I/O 就绪事件,而是关注接收完成的事件
  • 事件复用器等待操作完成
  • 当事件复用器等待时,OS 并行的在内核线程中执行读操作,将数据放入用户定义的缓冲区,读取完成后通知事件多路复用器
  • 事件复用器调用适当的处理程序
  • 处理程序处理用户定义缓冲区的数据,启动新的异步操作,并将控制返回给事件多路复用器

4. 小结

I/O 模型经常对比的是阻塞和非阻塞,还有同步和异步,在《UNIX 网络编程》第6章已经给出了它们的区别,并且给出的图示很直观。这里简单做下说明,以 UDP 为例,读取一个数据包可以分为两个阶段:

  • 第一阶段:应用进程发起系统调用,内核无数据包准备好,等待数据
  • 第二阶段:数据包准备好,OS 将数据从内核复制到用户空间

阻塞和非阻塞描述的是第一阶段无数据可读取时线程是否挂起;同步和异步描述的是第二阶段,在数据复制过程中线程是否参与和挂起。注意 NIO/BIO 都是同步 I/O,NIO 对应 UNP 中描述的 I/O 复用模型。

智能推荐

注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
© 2014-2019 ITdaan.com 粤ICP备14056181号  

赞助商广告