Java NIO 与IO区别
在研究Java NIO和IO API时,很快就会想到一个问题:
什么时候应该使用IO,什么时候应该使用NIO?
在本文中,我将尝试阐明Java NIO和IO之间的差异,它们的用例以及它们如何影响代码的设计。
1 Java NIO和IO的主要区别
下表总结了Java NIO和IO之间的主要区别。我将在表格后面的各节中详细介绍每种差异。
IO | NIO |
---|---|
面向流 | 面向缓冲区 |
阻止IO | 非阻塞IO |
无选择器 | 有选择器 |
2 面向流与面向缓冲区
Java NIO和IO之间的第一个大区别是IO是面向流的,而NIO是面向缓冲区的。那是什么意思呢?
面向流的Java IO意味着您一次从流中读取一个或多个字节。您对读取字节的处理取决于您自己。它们不会在任何地方缓存。此外,您无法在流中的数据中来回移动。如果需要来回移动从流中读取的数据,则需要先将其缓存在缓冲区中。
Java NIO的面向缓冲区的方法略有不同。数据被读入缓冲区,以后再从缓冲区中进行处理。您可以根据需要在缓冲区中来回移动。这使您在处理过程中更具灵活性。但是,您还需要检查缓冲区是否包含您需要的所有数据,以便对其进行完全处理。并且,您需要确保在将更多数据读入缓冲区时,不要覆盖尚未处理的缓冲区中的数据。
3 阻塞与非阻塞IO
Java IO的各种流正在阻塞。这意味着,当线程调用aread() 或时write(),该线程将被阻塞,直到有一些数据要读取或数据被完全写入为止。在此期间,线程无法执行其他任何操作。
Java NIO的非阻塞模式使线程可以请求从通道读取数据,并且仅获取当前可用的数据,或者如果当前没有可用数据,则什么也没有。线程可以继续进行其他操作,而不是在数据可供读取之前保持阻塞状态。
非阻塞写入也是如此。线程可以请求将某些数据写入通道,但不等待将其完全写入。然后线程可以继续运行,同时执行其他操作。
当没有阻塞在IO调用中时,哪些线程会花费空闲时间,通常在此期间在其他通道上执行IO。也就是说,单个线程现在可以管理输入和输出的多个通道。
4 选择器
Java NIO的选择器允许单个线程监视多个输入通道。您可以使用选择器注册多个通道,然后使用一个线程“选择”具有可用于处理输入的通道,或者选择准备好进行写入的通道。这种选择器机制使单个线程可以轻松管理多个通道。
5 NIO和IO如何影响应用程序设计
您是选择NIO还是IO作为IO工具包,可能会影响应用程序设计的以下方面:
- API调用NIO或IO类。
- 数据处理。
- 用于处理数据的线程数。
5.1 API调用
当然,使用NIO时的API调用看起来与使用IO时的API调用不同。这不足为奇。不仅要从例如an逐字节读取数据字节,还InputStream必须先将数据读入缓冲区,然后再从那里进行处理。
5.2 数据处理
当使用纯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() 电话返回时,您知道此行包含年龄等。
如您所见,该程序仅在有新数据要读取时才继续运行,并且对于每个步骤,您都知道该数据是什么。一旦执行线程的进度超过了读取代码中的特定数据段,该线程就不会在数据中向后移动(大多数情况下不会)。此原理也在此图中说明:
Java IO:从阻塞流读取数据。 |
NIO实现看起来会有所不同。这是一个简化的示例:
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
请注意第二行,该行从通道读取字节到中ByteBuffer。当该方法调用返回时,您不知道所需的所有数据是否都在缓冲区内。您所知道的是缓冲区包含一些字节。这使处理有些困难。
试想一下,如果在第一次read(buffer)调用后,读入缓冲区的所有内容只有半行。例如,“名称: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:从通道读取数据,直到所有需要的数据都在缓冲区中为止。 |
6 总结
NIO允许您仅使用一个(或几个)线程来管理多个通道(网络连接或文件),但是代价是解析数据可能比从阻塞流中读取数据更为复杂。
如果您需要同时管理数千个打开的连接(每个连接仅发送少量数据),例如聊天服务器,则在NIO中实现该服务器可能是一个优势。同样,如果您需要保持与其他计算机的大量开放连接,例如在P2P网络中,则使用单个线程来管理所有出站连接可能是一个优势。下图说明了这种单线程多连接设计:
Java NIO:管理多个连接的单个线程。 |
如果只有很少的连接具有很高的带宽,一次要发送大量数据,那么经典的IO服务器实现也许是最合适的选择。下图说明了经典的IO服务器设计:
Java IO:一种经典的IO服务器设计-一个线程处理一个连接。 |
热门文章
优秀文章