Java NIO-12.NIO和IO

学习了Java NIO和IO API之后,就有了一个问题:
什么时候用IO,什么时候用NIO?
本文将试着阐明Java NIO和IO之间使用上的区别,以及它们是如何影响到你的代码设计的。

Java NIO和IO之间的主要区别

IO NIO
面向流 面向缓冲区
阻塞IO 非阻塞IO
选择器

下面的表格总结了Java NIO和IO的区别。表格后面对更多的细节进行说明。

IO NIO
面向流 面向缓冲区
阻塞IO 非阻塞IO
选择器

面向流与面向缓冲区

第一个大的区别就是IO是面向流的,而NIO是面向缓冲区的。什么意思呢?
Java IO是面向流的,就是说从流中一次性读取一个或者多个字节。无论读取出来的数据怎样使用,它们都不会被缓存。此外,流中的数据也不能被前后移动。如果需要前后移动流中的数据,就需要先将它们存在缓冲区中。
Java NIO的面向缓冲区方式有点不同。数据被读到一个稍后才使用的缓冲区。缓冲区中的数据能根据需要前后移动。这样在处理中提供了很大的灵活性。但是,也需要检查缓冲区中的数据是否包含了需要处理的所有数据。此外,往缓冲区中读取更多的数据时,需要确认没有覆盖掉还未处理的数据。

阻塞和非阻塞IO

Java IO中的各种流是阻塞的。这意味着当一个进程执行读或写的操作时,线程在读到数据或者写入完成之前,都是阻塞地。这期间进程不能进行任何操作。
Java NIO的非阻塞模式使线程能够从通道请求读取数据,仅得到当前可用的部分,如果当前没有数据可用就什么都得不到。而不是在数据可读之前保持阻塞,线程能继续处理其他的事情。
非阻塞写是一样的。线程能请求向通道写入数据,但不会等到完全写入。这期间线程能够处理别的事情。
在IO请求的非阻塞空闲期间,线程通常在处理其他通道的IO。这样,一个线程能够处理多个通道的输入输出。

选择器(Selectors)

Java NIO的选择器让一个线程能够监控多个通道的输出。能够在一个选择器上注册多个通道,然后用这一个线程去“选择”有了要处理的可用数据的通道,或者“选择”准备好写入的通道。选择器简化了单线程控制多通道的工作。

NIO和IO对应用设计的影响

IO工具箱是选择IO还是NIO可能在以下方面影响程序设计:

  1. 调用NIO还是IO类的API。
  2. 数据处理
  3. 处理数据的线程数

API调用

当然使用NIO和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();

注意处理状态由程序执行多久,换句话说,一旦第一个read.readLine()方法返回,就能肯定文本的整第一行都读取了。readLine()在整行读取完成之前都是阻塞的,这就是为什么能肯定这一行里面包含了姓名。同样的,当第二行readLine()返回,这一行的数据中肯定包含年龄。
可以看到,处理程序仅在有新数据读入时运行,每一步读入的数据都知道是什么。一旦运行中的线程处理了读入的数据,该线程不会回退数据(大多如此)。下图是整个流程:


Java NIO-12.NIO和IO_第1张图片
Reading data from a blocking stream

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()方法必须跟踪有多少数据读入了缓冲区中,返回真或者假,取决于缓冲区是不是满的。换句话说,如果缓冲区准备好处理了,它被认为是满的。
bufferFull()方法扫描整个缓冲区,但是在它返回之前,缓冲区必须保持同样的状态。如果没有,下一次读取到缓冲区中的数据就不能在正确的位置读取。这不是不可能的,但却是又一个需要注意的问题。
如果缓冲区满了,就能被处理。如果不是满的,处理一部分也行的话,那无论数据在不在里面都可以先部分处理。大多数情况下都没意义。


Java NIO-12.NIO和IO_第2张图片
Reading data from a channel until all needed data is in buffer

总结

NIO让一个(或几个)线程可以处理多个通道(网络连接或文件),但是代价就是解析可能比从阻塞的流中读取数据更复杂。
如果同时管理上千个打开的连接,每个连接只发送一点点数据,例如聊天服务器,用NIO实现这种服务器是有优势的。类似的,如果需要保持很多和其他电脑的连接,例如P2P网络,用单个线程处理所有的出站连接,也可能有优势。下图是一个线程处理多个连接的示例图:


Java NIO-12.NIO和IO_第3张图片
A single thread managing multiple connections

如果要处理少量高带宽的连接,一次发送大量的数据,可能传统的IO服务器实现起来合适点。下面是传统IO服务器的示例图:


Java NIO-12.NIO和IO_第4张图片
A classic IO server design - one connection handled by one thread.

你可能感兴趣的:(Java NIO-12.NIO和IO)