在学习Java NIO 和 IO API's 的时候,一个问题在我脑海闪过:
我什么时候应该用 IO而什么时候应该用NIO呢?
在本文,我会尝试带来关于Java NIO与IO区别的一些启发,它们的使用场景,还有它们会怎样影响你的代码设计。
Java NIO 与 IO的主要区别
下表简单说明Java NIO和IO的主要区别。我会针对表中每个不同点在下面的章节展开详细说明。
IO | NIO |
---|---|
面向流 | 面向缓冲区 |
阻塞IO | 非阻塞IO |
选择器 |
面向流 vs. 面向缓冲区
Java NIO 与 IO 第一个较大的不同点在于,IO 是面向流的,而NIO则是面向缓冲区。那么,这究竟是什么意思?
Java IO 面向流意味着你每次从一个流中读一个或多个字节。你怎么处理读取的字节取决于你。他们没有任何地方可以做缓存。此外,你不能在流的数据中来回移动。如果你需要在数据中来回移动,你还需要先把它缓存到缓冲区。
Java NIO的面向缓冲区的方法稍微不同。数据是读进缓冲区随后被处理的。只要你需要,你可以在缓冲区来回移动。这会给你在处理的时候更多的灵活性。然而,在全部处理完后你仍需要检查缓冲区是否包含你需要的全部数据。然后,你需要保证在读入更多数据到缓冲区的时候,不会覆盖你在缓冲区还没处理好的数据。
阻塞 vs. 非阻塞IO
Java IO的各种各样的流都是阻塞的。这意味着,当一个线程调用 read() 或 write()的时候,线程会一直阻塞直至数据全部读完或全部写完。此线程在这个时候不能做其他任何事。
Java NIO的非阻塞模式,允许一个线程在当前channel中有可用的数据从Channel中获取数据,如果当前的数据不可用,则不获取。而不会阻塞在这里直至数据可用,这个线程能继续去做其他事情。
非阻塞写也是如此。一个线程可以将数据写进channel,而无需等待它全部写完。线程能继续去做其他事情。
在非阻塞IO调用情况下,线程花费闲置时间通常用于在其他channel执行IO。这就是为什么一个线程可以处理多个channel的输入和输出。
选择器
Java NIO的选择器允许单线程监控多个输入的channel。你可以在一个选择器上注册多个channel,然后用单线程去“选择”当前需要处理的channel,或者选择等待处理的channel。选择器机制让单线程管理多个channel变得容易。
NIO 和 IO 怎样影响应用设计
不论你选择 NIO 还是 IO 作为你的工具包,可能影响你的应用设计的一下方面:
- API 调用 NIO 或者 IO 的类。
- 数据的处理。
- 用于处理数据的线程数。
API 调用
当然是API调用NIO还是IO,看起来是不同的。没有什么惊喜。不只是从输入流中由字节到字节读取数据,数据必须先读入缓冲区,然后才能进行处理。
数据处理流程
数据的处理流程同样受到使用纯 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()方法会阻塞直至整行读完,这就是原因。同时你知道这行包含了 name的内容。类似地,当第二个reader.readLine()调用返回时,你知道这行包含了age 的内容。
如你所见,程序仅仅在有新的数据读取的时候处理,而且每一步你都知道数据是什么。一旦执行线程在代码中已经处理完一段特定的数据,这个线程不会在数据中向后移动(基本不会)。下图阐述了文中说的原理:
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() 方法可以跟踪多少数据读进缓冲区,并且根据缓冲区是否已经满,返回 ture 或者 flase。换句话说,当缓冲区满 的时候,它就准备好被处理了。
bufferFull() 方法扫描缓冲区,但必须让缓冲区保持与调用bufferFull()方法之前相同的状态。否则,下一个数据被读进缓冲区的位置可能是不正确的。这不是不可能的,不过这是另一个需要当心的问题。
如果缓冲区是慢的,那么它可以被处理。但如果它还没满,你也可以部分处理在那里的数据,在特定的情况下这也是合理的。大部分情况则不是。
以下是 数据是否在缓冲区准备好的原理图:
总结
NIO 允许你用单(或多)线程管理多个channel(网络连接或文件),但处理数据的成本比从阻塞流读取数据更高。
如果你需要同时处理数千个打开的连接,而仅仅是发送少量的数据,比如一个聊天服务,那么使用NIO实现可能更合适。同样地,如果你需要保持打开多个与其他电脑的连接,例如在P2P网络,用单线程去管理
你的出站网络,也是有优势的。以下是,单线程,多连接的原理图:
如果你只有少数连接而且需要很高的带宽,同一时间发送大量数据,可能经典的IO服务实现会是最合适的。以下是经典IO服务设计的原理图: