最后更新时间:2014-06-23
当学习Java NIO和IO的API的时候,一个问题很快的就会出现中我们的脑中:
我什么时候应该使用IO,什么时候应该使用NIO?
在这篇文章中我将会尝试着写出中NIO和IO之间不同的地方,他们的使用场景,以及他们怎么影响你的代码设计。
Java NIO和IO的主要不同
下面的表格总结了Java NIO和IO的主要不同。针对这个表格中的不同点我将会给予更加详细的说明。
IO | NIO |
基于流的 | 基于缓冲区的 |
堵塞IO | 非堵塞IO |
Selectors(选择器) |
基于流的相对基于缓冲区的
在Java NIO和IO之间第一个最大的不同就是,IO是基于流式的,而NIO是基于缓冲区的。这样是意味着什么呢?
基于流式的Java IO意味着你每次是从一个流中读取一个或者更多的字节。处理这个读到的字节取决于你。他们不能在任何地方被缓存。并且,在流中的数据你不能向前或者返回。如果你需要去向前或者返回去读取一个流中的数据,你将会首先需要把它缓存中buffer中。
Java NIO基于缓冲区的方式稍微有些不同。数据读进一个缓冲区中,然后后面会被处理。你可以按照你需要的在缓冲区中向前或者返回。这个中处理期间给予更大的灵活性。然而,你也需要去检查这个缓冲区中是否包含你需要处理的所有数据。而且,你需要确认什么时候可以读取更多的数据进入到缓冲区,你不能覆盖掉还没有处理的缓冲区的数据。
堵塞相对非堵塞IO
Java IO的各种流是堵塞的。那就意味着,当一个线程调用read()方法或者write()方法的时候,那个线程将会被堵塞直到这里有数据可以读,或者有数据可以被写。在此期间,这个线程不能做其他任何事情。
Java NIO的非堵塞模式可以让一个线程从一个通道中发送读数据请求,并且只是得到当前可用的,或者如果当前没有数据可用,那就什么都没有。而不是保持锁定直到有数据可读,这个线程可以继续去做其他的事情。
对于非堵塞写也是相同的。一个线程可以发送一个写数据到通道的请求,但是不会等到它完全被写。这个线程然后继续的同时去做其他事情。
当非堵塞IO调用的时候,线程花费他们空闲的时间在什么地方?通常是同时在其他的通道中执行IO。那就是,一个单独的线程可以管理多个通道的输入和输出。
选择器(Selectors)
Java NIO的选择器允许一个单独的线程去监控多个通道的输入。你可以用一个选择器注册多个通道,然后使用一个单线程去“选择”对于进程有可用输入的通道,或者“选择”准备好写的通道。这个选择器原理使得单个线程去管理多个通道变得简单了。
NIO和IO是怎样影响应用设计
不管你选择NIO还是IO作为你的工具包可能都会影响你应用设计的各个方面:
- 对于NIO或者IO类的API调用。
- 数据的处理。
- 处理数据的线程数量
API调用
当然使用NIO对于使用IO的API调用看起来是不同的。这没有什么惊讶的。Java NIO不是从例如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)调用之后,所有的读到缓冲区的数据是一行的一半。例如,“Name: An”。你能处理那个数据吗?真的不能。你需要等待直到一个完整的数据行读取到了缓冲区中,在有意义的处理这个数据的任何部分之前。
以至于为了能够有意义的去处理数据,你怎么知道这个缓冲区包含了足够的数据呢。好吧,你不知道。发现的唯一方式就是,去看这个缓冲区的数据。这个结果就是,在你知道是否所有的数据在这里之前,你可能不得不检查几次这个缓冲区中的数据。这样不仅是低效的,而且在代码设计方面变的混乱的。例如:
ByteBuffer buffer = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buffer); while(! bufferFull(bytesRead) ) { bytesRead = inChannel.read(buffer); }
这个bufferFull()的方法不得不跟踪有多少数据读到缓冲区了,并且根据缓冲区是否满了返回true或者false。换句话说,如果缓冲区准备好处理了,它就是认为满了。
这个bufferFull()方法扫描这个缓冲区,但是在这个bufferFull()方法被调用之前一定要处在缓冲区相同的位置。如果不是的话,下一个读取到buffe的数据可能就不会读到正确的位置上。这个不是不可能的事情,但是这个仍然是需要注意的另外一个问题。
如果缓冲区满了,它就可以被处理了。如果不满,如果那个在你的特别的场景中有意义的,你可能不管什么样的数据在这里,能够部分的处理数据。在大多数场景下是不可以的。
这个过程如下图所示:
总结
NIO允许你去管理多个通道(网络连接或者文件)使用一个单独的线程或者多个线程,但是这个代价就是解析数据可能稍微复杂一些相对从一个堵塞的流中读取数据而言。
如果你需要同时管理成千上万个打开的连接,这些连接每一个都要发送一些数据,例如一个聊天服务器,用NIO实现这个服务器可能是一个优势。相似的,如果你需要对其他的计算机保持许多的打开的连接,例如中一个P2P网络中,使用一个单独的线程管理你的所有的外部的连接可能是一个优势。这个一个线程,多个连接设计如下面的示意图所示:
如果你有很少的连接伴随着非常高的带宽,每次发送许多数据,可能一个标准的IO实现是更好的选择。如下图所示:
翻译地址:http://tutorials.jenkov.com/java-nio/nio-vs-io.html