在学习 Java NIO 和 IO API 时,很快你就会问自己:什么时候该用 IO,什么时候使用 NIO?
在本文中,将试图阐明 Java NIO 和 IO 之间的区别、它们的用例以及它们如何影响代码的设计。

阅读文章的过程中如果有任何疑问,欢迎添加笔者为好友,拉您进【七日书摘】微信交流群,一起交流技术,一起打造高质量的职场技术交流圈子,抱团取暖,共同进步。
七日书摘官方群.jpg

NIO 和 IO 之间的主要差异(Mian Differences Between Java NIO and IO)

下面这个表格概括了 NIO 和 IO 的主要差异。后续文章章节中会详细地描述表中每部分的差异。

IONIO
Stream orientedBuffer oriented
Blocking IONon blocking IO
-Selectors

面向流和面向缓冲区比较(Stream Oriented vs. Buffer Oriented)

第一个重大差异是 IO 是面向流的,而 NIO 是面向缓冲区(buffer)的。这句话是什么意思呢?

Java IO 面向流的意思是我们每次从流当中读取一个或多个字节。怎么处理读取到的字节是我们自己的事情。他们不会再任何地方缓存。再有就是我们不能在流数据中向前后移动。如果需要向前后移动读取位置,那么我们需要首先为它创建一个缓冲区(buffer)。

Java NIO 是面向缓冲区(buffer)的,这有些细微差异。数据是被读取到缓冲区(buffer)当中以便后续加工。我们可以在缓冲区(buffer)中向前后移动。这个特性给我们处理数据提供了更大的灵活空间。当然我们仍然需要在使用数据前检查缓冲区(buffer)中是否包含我们需要的所有数据。另外需要确保在往缓冲区(buffer)中写入数据时避免覆盖了已经写入但是还未被处理的数据。

阻塞和非阻塞 IO 比较(Blocking vs. No-blocking IO)

Java IO 中的各种流都是阻塞的。这意味着一个线程一旦调用了 read(),write() 方法,那么该线程就被阻塞住了,直到完全读取到数据或者数据完整写入了。在此期间线程不能做其他任何事情。

Java NIO 的非阻塞模式使线程可以通过 channel 来读数据,并且只获取当前可用的数据,或者当前没有可用的数据,则不获取任何数据。这样线程不会被阻塞住,它可以继续向下执行。

对于非阻塞写入也是如此,线程可以请求将某些数据写入通道(Channel),且不用等待其完全写入。同时,线程也可以继续并在同一时间做其他事情。

通常线程在调用非阻塞操作后,会通知处理其他 channel 上的 IO 操作。因此一个线程可以管理多个 channel 的输入输出。

选择器(Selectors)

Java NIO 的选择器(Selector)允许一个单独线程监听多个输入通道(Channel)。可以注册多个通道(Channel)到一个选择器(Selector)上,然后使用一个单独的线程来“选出”处于可读或者可写状态的通道(Channel)。选择器(Selector)机制使得单线程管理多个通道(Channel)变得容易。

NIO 和 IO 是如何影响程序设计的(How NIO and IO Influences Application Design)

无论您选择IO或NIO工具箱,可能会影响您应用程序设计的以下几个方面:

  1. 对NIO或IO类的API调用;
  2. 数据处理;
  3. 用来处理数据的线程数;

API 调用(The API Calls)

显然,使用 NIO 的 API 接口和使用 IO 是不同的,但这并不意外,不同于直接从 InputStream 读取字节,我们的数据需要先写入到 buffer 中,然后再从 buffer 中处理它们。

数据处理(The Processing of Data)

当使用纯 NIO 设计而不是 IO 设计时,数据的处理也会受到影响。

在IO设计中,你可以从 InputStream 或者 Reader 中逐字节读取。假设你正在处理基于行的的文本数据流。例如:

Name: Anna
Age: 25
Email: anna@mailserver.com
Phone: 1234567890

该文本的数据流可以这样处理:

InputStream input = ... ; // get the InputStream from the client socket

BufferedReader reader = new BufferedReader(new InputStreamReader(input));

String nameLine   = reader.readLine();
String ageLine    = reader.readLine();
String emailLine  = reader.readLine();
String phoneLine  = reader.readLine();

注意处理状态是由程序执行多久决定的。换言之,一旦第一个 reader.readLine() 方法返回,你就可以确定已经读取了整行文本内容。 readline() 方法会阻塞直到整行读完,这就是原因。你也知道此行包含名称。类似地,当第二个 readLine() 方法调用返回时,你直到这一行包含 age 等。

如你所见,程序仅在有新数据要读取时才运行,并且对于每个步骤你都知道每步的数据是什么。一旦正在运行的线程已处理过读入的某个数据之后,该线程不会再回退数据(大多如此)。下图也说明了这条原则:
nio-vs-io-1.png
Java IO: Reading data from a blocking stream.

NIO 的实现会有所不同。下面是一个简单的例子:

ByteBuffer buffer = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buffer);

注意第二行,从通道(Channel)读取字节数据到 ByteBuffer。当该方法调用返回时,你不知道你需要的所有数据是否都在缓冲区(buffer)内。你只知道该缓冲区(buffer)包含一些字节,这使得处理变得有些困难。

想象一下,在第一个 read(buffer)调用之后,所有被读入缓冲区(buffer)的内容都是半行。例如,"Name:An" , 你能处理这些数据吗? 显然不能,需要等待直到整行数据读入缓存,再次之前对数据的任何处理都毫无意义。

那么,如何知道缓冲区(buffer)是否包含足够的数据可以有效地进行处理呢?好吧,你不知道。唯一的办法是查看缓冲区中的数据。其结果是,在知道所有数据是否都在缓冲区中之前,你可能需要多次检查缓冲区(buffer)中的数据。这不仅效率低下,而且在程序设计方面可能变得混乱复杂。例如:

ByteBuffer buffer = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buffer);

while(! bufferFull(bytesRead) ) {
    bytesRead = inChannel.read(buffer);
}

bufferFull() 方法必须跟踪读入缓冲区(buffer)的数据量,并根据缓冲区(buffer)是否已满返回 true 或 false。换句话说,如果缓冲区(buffer)已准备好进行处理,则表示缓冲区(buffer)已满。

bufferFull() 方法扫描缓冲区(buffer),但必须使缓冲区保持与调用 bufferFull()方法之前相同的状态。否则,下一个读入缓冲区的数据可能无法从正确的位置读入。这并非不可能,但这是另一个需要注意的问题。

如果缓冲区(buffer)已满,则可以对其进行处理。如果它未满,你可以处理其中任何的部分数据,如果这在你的特定情况下是有意义的。但在很多情况下并非如此。

下图展示了“缓冲区(buffer)数据循环就绪”:
nio-vs-io-2.png
Java NIO: Reading data from a channel until all needed data is in buffer.

小结(Summary)

NIO 允许你仅使用一个(或几个)线程管理多个通道(网络连接或文件),但代价是解析数据可能比从阻塞流中读取数据复杂。

如果您需要同时管理成千上万个打开的连接(每个连接只发送少量数据,例如聊天服务器),那么使用 NIO 实现服务器是有优势的。类似地,如果您需要维持大量的连接,例如 P2P 网络,使用单线程管理所有出站连接是有优势的。单线程多连接设计如下图所示:
nio-vs-io-3.png
Java NIO: A single thread managing multiple connections.

如果连接数不是很多,但是每个连接都占用较大带宽,每次都要发送大量数据,那么使用经典的 IO 设计服务器可能是最好的选择。下面是经典 IO 服务设计图:
nio-vs-io-4.png
Java IO: A classic IO server design - one connection handled by one thread.

英文原文链接:http://tutorials.jenkov.com/java-nio/nio-vs-io.html

------完------

推荐阅读:

Java NIO 简明教程 之 Java NIO 套接字通道(SocketChannel)

Java NIO 简明教程 之 Java NIO 服务端套接字通道(ServerSocketChannel)

Java NIO 简明教程 之 Java NIO 非阻塞式服务器(Non-blocking Server)

Java NIO 简明教程 之 Java NIO 数据报通道(DatagramChannel)

Java NIO 简明教程 之 Java NIO 管道(Pipe)

Java基础知识面试题篇(2020年2月最新版)

更技术学习请进入七日书摘官方群: 七日书摘官方群

七日书摘官方群群聊二维码.png

参考资源:
https://blog.csdn.net/Andrew_Yuan/article/details/80215164
https://wiki.jikexueyuan.com/project/java-nio-zh/java-nio-pipe.html