本文主要记录 Java 中 I/O 模型及相关知识点 。另,由于自身知识水平有限,如有不正之处,望各位能够谅解,欢迎批评指正!
一、I/O 基础
由于 Java 中 I/O 相关的 API ,无论是 BIO 还是 NIO,都是相对抽象的。所以我先整理一下 I/O 的基础知识,了解一下操作系统是如何处理 I/O 操作的。这一部分的内容,主要来自于『Java NIO』一书,中译版的 1.4 节:I/O 概念。
1.1 缓冲区操作
缓冲区,以及缓冲区如何工作,是所有 I/O 的基础。I/O 操作也就是 input/output,输入/输出操作,实际上就是将数据移进/移出缓冲区。进程执行 I/O 操作,归结起来,也就是向操作系统发出请求,让它将缓冲区排干(写),或者用数据将缓冲区填满(读)。这里面的读可能好理解一点,写估计就有点不太直白了。个人理解:就是将缓冲区之前写入的数据移出去,也就是发送,感觉和 Java 中 I/O 操作的 flush() 方法有点类似的感觉。
下图简单描述了数据从外部磁盘向运行中的进程的内存区域移动的过程。进程使用 read() 系统调用,要求其缓冲区被填满。内核随即向磁盘控制硬件发出命令,要求其从磁盘读取数据。磁盘控制器把数据直接写入内核内存缓冲区,当磁盘控 制器把缓冲区装满,内核即把数据从内核空间的临时缓冲区拷贝到进程执行 read() 调用时指定的缓冲区。
注意,图中用户空间和内核空间的概念。用户空间是常规进程所在区域,是非特权区域:比如,在该区域执行的代码不能直接访问硬件设备。内核空间是操作系统所在区域,是特权区域:它能与设备控制器通讯,控制着用户区域 进程的运行状态,等等。最重要的是,所有 I/O 都直接或间接通过内核空间。
为什么 I/O 操作都要直接或间接的经过内核空间?我能想到的有两点:一是安全,二是效率。书中也介绍了分散/汇聚高效的处理数据原因。
二、BIO 和 NIO
在前面演示的 I/O 过程中,大致分为两个步骤:
- 用户空间的进程发起 I/O 请求,操作系统接收到请求后,系统内核将数据从硬件中读取到内核缓冲区(准备需要的数据);
- 内核空间的系统内核将数据从内核缓冲区移动到用户空间的缓冲区,然后数据可供用户进程使用(填充缓冲区)。
显然,这两个步骤或多或少都会消耗一定的时间。那么 BIO 和 NIO 的区别就在这两个步骤中区别开了。
2.1 BIO
IO 称之为阻塞 I/O。在第一个阶段,用户进程发起 I/O 请求后,程序会处于阻塞状态,直到内核将数据准备好。然后,在第二个阶段,用户进程仍然处于阻塞状态,直到内核完成用户空间缓冲区的填充,最后用户进程开始使用数据。也就是说用户进程从发起请求的那一刻,直到数据可用,一直都处于阻塞状态。
2.2 NIO
NIO 称之为非阻塞 I/O。在第一个阶段,用户进程发起 I/O 请求后,如果内核未将数据准备好,请求会直接返回,并表明数据没有准备好。此时用户进程不会继续等待,会继续运行。一般我们都是重复的去检查内核是否将数据准备好。当数据准备好之后,进入第二阶段,发起填充缓冲区的请求,此时即便是 NIO 也会阻塞到用户空间的缓冲区填充完成。
由此,我们可以发现,BIO 和 NIO 的区别是在 I/O 操作的第一个阶段,而第二个阶段,两者都会阻塞。
在这里,我曾经有过这样的误会:既然 NIO 也是循环的去检查内核是否将数据准备好,程序本身就是要去处理即将获取的数据,那么它非阻塞的意义在哪儿呢,还不如直接阻塞的等待,何必浪费 CPU 不停的轮询呢?紧接着我看了多路复用 I/O,才算明白了 NIO 的优势。
三、多路复用 I/O
实际上 Java 中的 NIO 是多路复用 I/O 模型,真正的优势是其线程模型。而实现这个线程模型的关键在于其使用的系统调用是 select()。在介绍 select()调用前,先声明一点:Java 中的 NIO 非阻塞的特性仅适用于网络 I/O,所以下面主要针对网络 I/O 来讲。
select() 会返回当前所有数据准备就绪的 Socket,如果当前没有数据准备就绪的 Socket,select() 就会阻塞。那么只需要一个线程就能处理多个 Socket 的数据。回顾一下 BIO,当我们建立一个 Socket 连接时,这个建立连接的线程就会阻塞,直到有数据可用,也就是一个线程必须对应一个 Socket。
BIO 的劣势在于单机中线程数量是有限的,我觉得肯定比 Socket 连接数少很多吧。其次当线程数较多时,线程之间的切换会消耗掉很多的系统资源。并且当大部分的 Socket 连接的数据传输并不是很密集的时候,这些 Socket 对应的线程都处于阻塞状态,这显然是空占内存!此时就是使用 Java NIO 的绝佳机会。
可能说了那么多,大家都觉得 BIO 一无是处,Java 网络编程直接上 NIO 就 Okay!其实我觉得两者都有自己适用的环境,没有最好的,只有更合适的。
- BIO 适用场景:
普通的文件读写、Socket 连接数量较少、数据传输密集等场景。在网络编程中,一般用于客户端。 - NIO 适用场景:
普通的文件读写、Socket 连接数较多、需要考虑高并发等场景。在网络编程中,一般用于服务端。
四、AIO
AIO 称之为异步 I/O。AIO 是最理想的 I/O 处理方式。在 I/O 操作的两个阶段均不阻塞,当用户程序发起 I/O 请求时直接返回。然后系统内核完成接下的所有操作之后,会通知用户程序直接使用数据。
由于第二个阶段是由系统内核主动完成的,所以 AIO 需要操作系统底层的支持。
五、总结
下面是我在网上扒来的一张图,很好的说明了各类 I/O 模型。
关于同步与异步、阻塞与非阻塞,文末给出的参考资料值得一看。
参考资料:
- 网络IO模型 - leung - 开源中国 文中的流程图非常好,有助于理解
- [思考] 也谈同步异步I/O - SmithFox
- Java NIO:浅析I/O模型 - 海 子 - 博客园
- 怎样理解阻塞非阻塞与同步异步的区别? - 知乎 参考排名较高的回答
- IO - 同步,异步,阻塞,非阻塞 (亡羊补牢篇) - 智障大师 的专栏 - CSDN博客 文末的比喻很形象