Java NIO 是 NIO 么?
本文转自“雨夜随笔”公众号,欢迎关注
网上目前很多文章,都是直接将 Java NIO 说成是非阻塞 IO,而且不乏很多大厂的文章,或者热点文章。但是我们要首先意识到这种说法是错误的。至于为什么,让我们来看一下:
I/O
I/O 在程序中指的 Input/Output,也就是数据的输入和输出。程序的 I/O 操作依赖于底层的 I/O 操作,基本上都会用上底层的 read/write 操作。而对于 read 和 write,需要知道的是 read 不是直接将数据直接从物理设备读取到内存中,而 write 也不是将数据直接从内存写入到物理设备。上层应用无论是调用系统的 read 还是 write,都涉及到内核缓存区,具体来说,调用操作系统的 read,是把数据从内核缓冲区复制到进程缓冲区;而 write 系统调用,是把数据从进程缓冲区复制到内核缓冲区。如下图:
缓存区的设置主要是为了减少频繁地与设备的物理交换。因为外部设备的直接读写,涉及操作系统的中断。发生系统中断时,需要保存之前的进程数据和状态等信息,而结束中断之后,还需要恢复之前的进程数据和状态等信息。为了减少这种底层系统的时间损耗、性能损耗,于是出现了内存缓冲区。
而缓存区的出现,导致实际上的 I/O 操作涉及到两个阶段:
等待数据准备好
将数据在内存缓存区之间进行拷贝
我们先记住这两个阶段,一般来说 IO 模型主要是在这两个阶段有区别。
Unix 中的 I/O
在了解 Unix 中的 I/O 之前,我们先了解两组名词:阻塞和非阻塞、同步和异步。
阻塞:阻塞意味着调用方在结果出现之前,不会做任何其他事情。
非阻塞:非阻塞意味着调用方在结果出现之前,同时在做其他的事情。被调用方会立即返回一个值,调用方拿到后可以做其他事情也可以选择不做。
同步:必须等待被调用方处理完请求返回结果。
异步:被调用方处理请求通过调用方注册的回调接口返回结果。
网上有各种各样的定义,根据我自己的理解,我认为阻塞和非阻塞主要针对调用方,它们的区别在于阻塞在得到结果前会一直等待,非阻塞会先得到一个值,但是在得到真正的结果前可以做其他的事情。而同步和异步主要针对被调用方的结果返回形式,同步是处理完再返回,异步是处理完后通过回调返回结果。
I/O 模型
上面两两组合,构成了 Unix 中的四种 I/O 模型。
同步阻塞 I/O (Blocking IO)
在阻塞式 IO 模型中,应用程序从 IO 系统调用开始,直到系统调用返回,在这段时间内,调用进程是阻塞的。如下图:
阻塞 IO 的优点是:实现简单,而且阻塞期间,用户线程基本不会占用资源。但是缺点也很明显:那就是在高并发的场景下,需要大量的线程来维护每一个阻塞的任务,内存和线程切换的开销都非常大。
同步非阻塞 I/O (Non-Blocking IO)
在 Linux 系统下,可以设置 socket 为非阻塞的的模式,在这种模式下,如果调用时没有数据,系统会立即返回一个调用失败的消息。然后调用方过段时间再查询,如果有数据,则变成阻塞方式,进行数据复制。而这个就是我们日常所说的 NIO。如下图:
非阻塞 IO 的优点在于:每次的调用都可以立即得到反馈,调用方不会阻塞,实时性比较好。缺点是:调用方需要不断的轮询,这个会占用 CPU 资源,并且效率低下。
I/O 多路复用 (IO Multiplexing)
为了避免非阻塞 IO 模型上轮询的问题,系统设计了 select/epoll,在这个模式下,一个进程可以监控多个文件描述符,也就是多个数据的就绪状态。目前支持 IO 多路复用的系统调用有 select,epoll 等,select 基本在所有的系统都支持,epoll 是 Linux 2.6 内核提出的,是 select 的一种增强版本。流程如下:
IO 多路复用模型是在非阻塞 IO 上的升级,优点是:不需要单独维护不同的连接,通过 select/epoll 可以通过一个选择器查询线程来处理上千万个连接。缺点是:IO 多路复用本质上还是属于同步 IO,也就是数据就绪后,还是需要阻塞等待数据复制完成。
异步 I/O (Asynchronous IO)
异步 IO 又称为 AIO,基本流程和上面不同,在于被调用方完成所有的操作后,再通知调用方。调用方得到通知后可以直接进行后续操作,而不需进行等待,如下图:
异步 IO 可以说是非常美好,有点非常明显,那就是:调用方永远不会阻塞。缺点是:因为被调用方完成了大部分工作,所以需要被调用方支持。目前 Windows 下有 IOCP 实现了异步 IO,Linux 下在 2.6 引入,但是还不完善,并且底层依旧采用 epoll,和 IO 多路复用相比,性能上没有明星的优势。
Java NIO
在上面我们说了 NIO 指的是 Non-Blocking IO,也就是非阻塞 IO。那么 Java NIO 是不是就是非阻塞 IO 呢?也就是当面试官问你:请你说一下 Java 的 NIO。你这时候直接说起了非阻塞 IO 是不是可以呢?
事实上,Java NIO 的实际全程是(Java New IO),主要是对为了弥补之前 IO(OIO)同步阻塞的缺点,相比于老的 IO 面向流,NIO 是面向缓冲区的。
Java NIO 的核心由三部分组成:
Channel:通道
Buffer: 缓冲区
Selector:选择器
有选择器,我们对比上面的四种模型就了解到 Java NIO 主要是基于 IO 多路复用这个 IO 模型。我们可以看一下 Java NIO 的模型:
我们这里限于篇幅,不细讲 Java NIO 的实现细节,我们主要讲一下 Java NIO 的设计哲学。
传统的 OIO 是面向字节流或者字符流的,总是以流式的方式顺序从流中读取字节。这样有个问题就是不能随意更改读取指针的位置,而 NIO 中为了解决这个问题,引入了 Channel 和 Buffer。读取和写入,只是从 Channel 中读取到 Buffer 里,或者从 Buffer 写入到 Channel 中。由于不是顺序操作,所以可以随意更改读取的位置。
传统的 OIO 因为是同步阻塞的,例如,传统的 IO 操作 read,当我们读取一个文件时,我们 read 的线程就会被阻塞住,直到 read 操作完成。那么 NIO 为了做到非阻塞,当我们调用 read 操作时,如果没有数据,read 会直接返回,不会阻塞当前线程。因为 NIO 使用了 Channel 和 Channel 的多路复用技术(IO 多路复用),后续可以通过 Selector 获取数据的就绪信息。
总结
Java NIO 是 Java New IO 的简称,是为了弥补之前 IO 的缺点。底层主要是基于 IO 多路复用的 IO 模型,并不简简单单的是 NIO(非阻塞 IO)。所以如果下一次面试官问你 Java NIO 和 NIO 的区别是什么?你要意识到他更希望你说出 IO 多路复用的细节和 Java NIO 中的 Buffer, Channel 和 Selector 以及他们的工作流程。
版权声明: 本文为 InfoQ 作者【soolaugust】的原创文章。
原文链接:【http://xie.infoq.cn/article/3d78ea75082348c654ebb3a53】。未经作者许可,禁止转载。
评论