在研究Java NIO和IO API时,很快就会出现一个问题:
我什么时候应该使用IO,什么时候应该使用NIO?
在本文中,我将尝试阐明Java NIO和IO之间的差异,它们的用例以及它们如何影响代码的设计。
Java NIO和IO的主要区别
下表总结了Java NIO和IO之间的主要区别。我将在表格后面的部分中详细介绍每个区别。
IO | NIO |
---|---|
流导向 | 缓冲导向 |
阻止IO | 非阻塞IO |
选择器 |
面向流的与面向缓冲的
Java NIO和IO之间的第一个重要区别是IO是面向流的,其中NIO是面向缓冲区的。那么,这意味着什么?
面向流的Java IO意味着你可以从流中一次读取一个或多个字节。你对读取的字节做什么取决于你。它们不会缓存在任何地方。此外,你无法在流中的数据中前后移动。如果需要在从流中读取的数据中前后移动,则需要先将其缓存在缓冲区中。
Java NIO的面向缓冲区的方法略有不同。数据被读入缓冲区,稍后处理该缓冲区。你可以根据需要在缓冲区中前后移动。这使你在处理过程中具有更大的灵活性。但是,你还需要检查缓冲区是否包含完整处理所需的所有数据。并且,你需要确保在将更多数据读入缓冲区时,不要覆盖尚未处理的缓冲区中的数据。
阻止与非阻塞IO
Java IO的各种流都是阻塞的。这意味着,当线程调用read()或write()时,该线程将被阻塞,直到有一些数据要读取,或者数据被完全写入。在此期间,该线程无法执行任何其他操作。
Java NIO的非阻塞模式允许线程请求从通道读取数据,并且只获取当前可用的内容,或者根本没有数据,如果当前没有数据可用。线程可以继续使用其他内容,而不是在数据可供读取之前保持阻塞状态。
非阻塞写作也是如此。线程可以请求将某些数据写入通道,但不要等待它完全写入。然后线程可以继续并在同一时间做其他事情。
什么线程在IO调用中没有阻塞时花费空闲时间,通常在此期间在其他通道上执行IO。也就是说,单个线程现在可以管理多个输入和输出通道。
选择
Java NIO的选择器允许单个线程监视多个输入通道。你可以使用选择器注册多个通道,然后使用单个线程“选择”具有可用于处理的输入的通道,或者选择准备写入的通道。这种选择器机制使单个线程可以轻松管理多个通道。
NIO和IO如何影响应用程序设计
无论你选择NIO还是IO,因为你的IO工具包可能会影响应用程序设计的以下方面:
- API调用NIO或IO类。
- 处理数据。
- 用于处理数据的线程数。
API调用
当然,使用NIO时的API调用看起来与使用IO时不同。这并不奇怪。而不是仅仅从例如InputStream读取字节的数据字节,必须首先将数据读入缓冲区,然后从那里进行处理。
数据处理
使用纯NIO设计与IO设计时,数据处理也会受到影响。
在IO设计中,你从InputStream或Reader中读取字节的数据字节。想象一下,你正在处理基于行的文本数据流。例如:
姓名:qq
年龄:7岁
电子邮件:[email protected]
电话:1234567890
这个文本行流可以像这样处理:
InputStream input = ...; //从客户端套接字获取InputStream
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()调用返回时,你知道此行包含年龄等。
正如你所看到的,只有当有新数据要读取时,程序才会进行,并且对于每个步骤,你都知道该数据是什么。一旦执行的线程已经超过读取代码中的某个数据片段,该线程就不会在数据中向后移动(通常不会)。此图中还说明了此原则:
|
NIO的实现看起来会有所不同。这是一个简化的例子:
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
注意第二行从通道读取字节到ByteBuffer。当该方法调用返回时,你不知道所需的所有数据是否都在缓冲区内。你只知道缓冲区包含一些字节。这使得处理更加困难。
想象一下,在第一次读取(缓冲)调用之后,是否所有读入缓冲区的内容都是半行。例如,“姓名:An”。你能处理这些数据吗?并不是的。在完成任何数据的处理之前,你需要等待至少一整行数据进入缓冲区。
那么你怎么知道缓冲区是否包含足够的数据来处理它?好吧,你没有。找出的唯一方法是查看缓冲区中的数据。结果是,在你知道所有数据是否存在之前,你可能需要多次检查缓冲区中的数据。这既低效又可能在程序设计方面变得混乱。例如:
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
while(!bufferFull(bytesRead)){
bytesRead = inChannel.read(buffer);
}
bufferFull()方法必须跟踪读入缓冲区的数据量,并返回true或false,具体取决于缓冲区是否已满。换句话说,如果缓冲区已准备好进行处理,则认为它已满。
bufferFull()方法扫描缓冲区,但必须使缓冲区保持与调用bufferFull()方法之前相同的状态。如果不是,则可能无法在正确的位置读入读入缓冲区的下一个数据。这不是不可能的,但这是另一个需要注意的问题。
如果缓冲区已满,则可以对其进行处理。如果它不满,你可能能够部分处理那里的任何数据,如果这在你的特定情况下是有意义的。在许多情况下,它没有。
这个图中说明了is-data-in-buffer-ready循环:
|
---|
Java NIO:从通道读取数据,直到所有需要的数据都在缓冲区中。 |
摘要
NIO允许你仅使用一个(或几个)线程来管理多个通道(网络连接或文件),但成本是解析数据可能比从阻塞流中读取数据时更复杂。
如果你需要同时管理数千个打开的连接,每个只发送一些数据,例如聊天服务器,在NIO中实现服务器可能是一个优势。同样,如果你需要与其他计算机保持大量开放连接,例如在P2P网络中,使用单个线程来管理所有出站连接可能是一个优势。此图中说明了这一个线程,多个连接设计:
|
---|
Java NIO:管理多个连接的单个线程。 |
如果你拥有较少带宽的连接,一次发送大量数据,那么可能最经典的IO服务器实现可能是最合适的。此图说明了经典的IO服务器设计:
|
---|
Java IO:经典的IO服务器设计 - 由一个线程处理的一个连接。 |