Java NIO与IO

n342 10年前

当学习java NIO和IO API时,大脑中会很快涌现一个问题:

什么时候用IO?什么时候用NIO?

这篇文章作者将尝试阐明Java NIO和IO之间的一些区别、它们的用例、它们各自是如何影响我们的代码设计的。

Java NIOIO的主要区别

以下表格简要说明了NIO和IO的区别,接着我们将详细说明表格中的每个不同点。

IO NIO
流式(Stream oriented) 缓冲式(Buffer oriented)
阻塞IO 非阻塞IO

选择器(Selectors)

面向流和缓冲型

Java NIO和IO第一个大的不同点是IO是面向流的,NIO则是缓冲型的。那么,这到底是什么意思?

Java IO是面向流的意味着我们从一个流中一次读取一个或多个字节。而要对读到的字节作何处理由我们自己决定;这其中没有任何缓存。此外,我们不能在数据流中来回移动;如果想要在从流读取的数据中来回移动,我们需要首先将数据缓存到缓冲区。

Java NIO的缓冲型方法稍有不同。数据读取到缓冲区后被加工,我们可以根据需求在数据中来回移动。这为处理提供了灵活性;然而为了充分处理所有数据我们还需要检查缓冲区是否包含所有需要的数据,并且我们需要确保读取更多数据到缓冲区时未被处理的数据不能被覆盖。

阻塞型IO和非阻塞型IO

Java IO的各种流是阻塞型的。这意味着,当一个线程调用read()方法或write()方法时这个线程将一直被阻塞,直到有数据被读到或者数据被完全写入;在被阻塞的同时,该线程不能做任何其他事情。

Java NIO的非阻塞模式允许一个线程从一个channel中请求读取数据,这只会取到当前有效的数据或当前没有数据有效时获取不到任何数据;而不是一直阻塞直到所读取数据准备好为止;在这同时该线程可以做其他事情。

这个过程对非阻塞式数据写入也是成立的。一个线程可以写入一些数据到channel,但是不用等待数据被完全写入。该线程在请求完成后可以继续同时去做其他事情。

当线程不在IO调用上被阻塞时,那么它们的空闲时间通常都花在了在其他channel上执行IO操作。也就是说,一个线程可以管理多个输入和输出的channel。

选择器(Selectors

Java NIO中的selector允许一个线程监视多个channel的输入。我们可以在一个selector上注册多个channel,然后使用一个线程来“选择”输入可用的channel来处理,或者选择准备好写入的channel。这种选择器模式使单个线程管理多个channel变的非常容易。

NIOIO对应用设计的影响

不论我们选择NIO还是IO作为我们的IO工具包都可能在以下几方面影响应用的设计:

  1. NIO或IO API类的调用

  2. 数据的处理

  3. 用于处理数据的线程数量

API调用

当然NIO API的调用和IO是不一样的,这没什么可奇怪的。与IO从流如InputStream中一个字节一个字节读取数据不同,使用NIO时数据必须先读到一个缓冲区中,然后再从缓冲区中处理。

数据处理

使用NIO设计和使用IO设计时数据的处理也会受影响。

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

Name: Anna

Age: 25

Email: anna@mailserver.com

Phone: 1234567890

使用流处理这个文本代码如下:

IInputStream input = ... ; // get theInputStream from the client socket

BufferedReader reader = newBufferedReader(new InputStreamReader(input));

String nameLine = reader.readLine();

String ageLine = reader.readLine();

String emailLine = reader.readLine();

String phoneLine = reader.readLine();

请注意,处理状态是有程序执行多远决定的。换句话说,一旦第一个reader.readLine()方法返回,我们就可以知道完整的一行文本读取完成;readLine()方法在一行读完之前一直保持阻塞状态,那就是原因;这一行包含name信息。相似的,当第二行readLine()方法返回,我们得到的是年龄信息,等等。

如我们所见,这个程序只有在有新数据可读的时候才向前执行,每一步我们都知道读到的数据是什么;一旦执行线程在代码中向前读取过一些数据,该线程在数据中将不能后退(绝大多数时候不能)。该规则如图所示:

图1:java IO 从阻塞流中读取数据

NIO的实现则有所不同,以下是代码例子:

ByteBuffer buffer =ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buffer);

请注意第二行从channel中往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()方法扫描缓冲区,但是在它被调用之前以相同的状态离开缓冲区。如果状态不相同,接下来读入缓冲区的数据可能不会读在正确的位置。这不是不可能的,但这是另一个需要注意的问题。

如果缓冲区完整,那么就可以被处理。如果不完整,在特定场景中允许的话,我们也许可以部分的处理已经在缓冲区中的数据。大多数情况下这种情况是不被允许的。

如下图展示的是“缓冲中数据是否准备好”的逻辑:

图2:Java NIO 从channel中读取数据直到所有数据都存入缓冲区

总结

NIO允许使用单个(或少量)线程来管理多种channel(网络连接或文件),但是代价是解析数据比使用阻塞流来读取数据更复杂。

如果同时需要管理数千连接,而每个连接只是发送少量数据,比如聊天服务器,使用NIO实现则比较有优势。相似的,如果需要保持很多连接和其他机器保持连接,如p2p网络,使用单线程去管理所有连接也许比较合适;这中单线程、多连接的设计图如下:

图3:Java NIO 一个线程管理多个连接

如果非常高的带宽下有很少连接一次性发送很多数据,那么经典的IO实现方式也许是最合适的。使用经典IO设计图如下:

图4:Java IO 经典IO服务器设计-一个连接由一个线程处理

1. 本文由程序员学架构翻译

2. 本文译自 http://java.dzone.com/articles/java-nio-vs-io

3. 转载请务必注明本文出自:程序员学架构(微信号:archleaner )