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工具包,可能会影响应用程序设计的以下方面:

  1. API调用NIO或IO类。
  2. 数据处理。
  3. 用于处理数据的线程数。

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服务器设计-一个线程处理一个连接。

热门文章

优秀文章