Java NIO vs. IO

在学习了Java NIO和IO API时,很快就会想到一个问题:什么时候应该使用IO,什么时候应该使用NIO?

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


1. Java NIO和IO的主要区别

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

IONIO
面向流
面向Buffer
阻塞IO非阻塞IO

Selectors


2.面向流 vs 面向缓冲区

Java NIO和IO之间的第一个大区别是:IO是面向流的,而NIO是面向缓冲区的。

面向流的Java IO意味着一次从流中读取一个或多个字节,对读取字节的处理取决于你自己,它们不会在任何地方缓存。此外,你无法在流中的数据中来回移动,如果需要在从流中读取的数据中来回移动,则需要先将其缓存在缓冲区中。

Java NIO面向缓冲区的方法略有不同。数据被读入缓冲区,并在以后进行处理。你可以根据需要在缓冲区中来回移动,这在处理过程中更具灵活性。但是,你还需要检查缓冲区是否包含你需要的所有数据,以便对其进行完全处理。并且,还需要确保在将更多数据读入缓冲区时,不要覆盖尚未处理的缓冲区中的数据。


3.Blocking vs. Non-blocking IO

Java IO的各种流都是阻塞IO。这意味着,当线程调用read()或write()时,该线程将被阻塞,直到有一些数据要读取或数据被完全写入为止。在此期间,线程无法执行其他任何操作。

Java NIO的非阻塞模式使线程可以请求从通道读取数据,并且仅获取当前可用的数据,或者如果当前没有可用数据,则什么都不获取。线程可以继续进行其他操作,而不是在数据可供读取之前保持阻塞状态。

非阻塞写入也是如此。线程可以请求将某些数据写入通道,但不必等待将其完全写入。然后,线程可以继续运行,同时执行其他操作。


4.Selectors

Java NIO的选择器允许单个线程监视多个通道。

你可以使用选择器注册多个通道,然后使用一个线程“选择”具有可用于处理的通道,或者选择准备好进行写入的通道。这种选择器机制使单个线程可以轻松管理多个通道。


5.NIO和IO如何影响应用程序设计

选择NIO还是IO作为IO工具包可能会影响应用程序设计的以下几个方面:

API调用。 

数据处理。 

用于处理数据的线程数。


API调用

使用NIO时的API调用看起来与使用IO时的API调用不同,这不足为奇。IO需要从流(InputStream等)中逐字节读取,而Nio则必须首先将数据读入缓冲区,然后再从那里进行处理。


数据处理

当使用纯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


NIO实现看起来会有所不同。这是一个简化的示例:

ByteBuffer buffer = ByteBuffer.allocate(48);



int bytesRead = inChannel.read(buffer);


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

想象一下,如果在第一次read(buffer)调用之后,读入缓冲区的所有内容都是半行。例如,“Name: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循环:

nio-vs-io-2.png


总结

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

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

nio-vs-io-3.png


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

nio-vs-io-4.png



 

展开阅读全文