Java NIO与IO

时间:2020-01-09 10:36:15  来源:igfitidea点击:

在研究Java NIO和IO API时,很快就会想到一个问题:

什么时候应该使用IO,什么时候应该使用NIO?

在本文中,我将尝试阐明Java NIO和IO之间的差异,它们的用例以及它们如何影响代码的设计。

Betwen Java NIO和IO的主要区别

下表总结了Java NIO和IO之间的主要区别。

IO </ b>NIO </ b>
面向流面向缓冲区
阻塞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时的API调用不同。这不足为奇。而不是仅从(例如)从一个字节读取数据字节。如果是InputStream,则必须先将数据读入缓冲区,然后再从那里进行处理。

数据处理

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

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

Name: Anna
Age: 25
Email: [email protected]
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()调用返回时,我们知道此行包含年龄等。

如我们所见,该程序仅在有新数据要读取时才继续运行,并且对于每个步骤,我们都知道该数据是什么。一旦执行线程的进度超过了读取代码中的特定数据段,该线程就不会在数据中向后移动(大多数情况下不会)。

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()"方法之前的状态相同的状态。如果不是,则下一个读入缓冲区的数据可能无法在正确的位置读入。这并非不可能,但这是另一个需要注意的问题。

如果缓冲区已满,则可以对其进行处理。如果还没有满,那么我们可能可以部分处理其中的任何数据,如果这在特定情况下有意义的话。在许多情况下,并非如此。

概括

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

如果我们需要同时管理数千个打开的连接(每个连接仅发送少量数据),例如聊天服务器,则在NIO中实现该服务器可能是一个优势。同样,如果我们需要保持与其他计算机的大量开放连接,例如在P2P网络中,使用单个线程来管理所有出站连接可能是一个优势。

如果只有很少的连接具有很高的带宽,一次要发送大量数据,那么经典的IO服务器实现也许是最合适的选择。