翻译-【Java NIO学习系列】Java NIO与IO

  翻译:http://tutorials.jenkov.com/java-nio/nio-vs-io.html

     当研究学习java NIO与标准IO的API时,一个问题马上涌入脑海:我应该什么时候使用NIO,什么时候使用IO,

     本文中我将阐述Java NIO和标准IO之间的差异,它们的使用场景,及它们在您代码设计中的影响。

     java NIO与IO的主要差异

     下表将总述java NIO与IO的差异,我讲在表格后面详细讲述各个部分的差异。

Java  标准IO Java NIO
面向流(Stream oriented 面向缓冲区(Buffer oriented
阻塞IO(Blocking IO 非阻塞IO(Non blocking IO
  选择器(Selectors

       面向流(Stream oriented)VS 面向缓冲区(Buffer oriented

       Java NIO与标准IO第一个最大的区别是:标准IO是面向流,NIO是面向缓冲区 。这是什么意思呢?

       Jave 标准IO面向流表示你可以在一个时间点里从流中读取一个或多个字节(bytes),你使用或处理完毕这些已经读取的字节后,它们也不会缓存在任何地方,此外你也不能前移后移在流中的数据,如果你要前移或后移在流中的数据,你应该将数据缓存在缓冲区(buffer)。

      Java NIO面向缓冲区方式稍有区别,将要被处理的数据被读入到缓冲区,在缓冲区里你可以按照你自己的需要前移或后移。这将给你在处理时带来极大的灵活性,但是为了成功的处理这些数据,你必须校验缓冲区是否包含你全部所需要的数据。同时你也的确认当读取更多的数据时,已经处理过的数据不会再次被读到缓冲区。

       阻塞IO(Blocking IO)VS非阻塞IO(Non blocking IO

      Java标准IO的各种流是阻塞的,这意味着当一个线程调用read() or write()时,线程将会被阻塞,直到一些数据被读取或者数据被完全写入的时,在此期间线程不能做任何其他事情。

     Java NIO的非阻塞方式可以使线程从通道(channel)里面读取数据,且只读取可用数据,如果当前没有可用数据,什么也不获取。此时并不将线程阻塞,直到数据变的可以读取期间,线程也可以做其他事情。

    非阻塞式写(non-blocking writing)同样适用。线程线程可以将数据写入通道,不需要等待完全写入,线程可以继续执行并执行其他操作。

     在非阻塞IO执行时,线程将空闲时间花费在同时执行的其他通道的IO上。这意味着,单个的线程可以管理多个通道的输入和输出。

     选择器(Selectors

     Java NIO选择器(Selectors)允许单个线程监控多个通道的输入和输出。你可以将多个通道注册在一个选择器(Selectors),然后使用该线程“选择”已经输入准备好处理的通道,或已经准备写入的通道。选择器机制使得单个线程方便的管理多个通道。

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

       无论你使用IO或者NIO作为你的工具箱,将会影响到你程序的一下几个方面:

      1.NIO或IO类的API调用

      2.数据处理

     3.数据处理的线程数

     API 调用

     当然使用NIO的API调用与IO的API调用看起来就存在差异,这并不奇怪,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()方法返回,你就可以确定一行完整的文本已经被读取。reader.readLine()将阻塞直到完整行读取完毕,这也是你为什么直到第一个包含的是name。类似,第二个reader.readLine()调用返回,你也直到该行包含的是age。

     正如你所见,该程序进行时,才会有新的数据读取,每一步,你知道这些数据是什么。一旦线程已经执行过某些数据,线程是不会的回退这些数据(大部分是不可以)。下图也说明了这条原则:

翻译-【Java NIO学习系列】Java NIO与IO

     NIO的实现有所不同,一下是简单的例子

 

ByteBuffer buffer = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buffer);

    注意的是从通道读取字节到字节缓冲区(ByteBuffer)的第二行。当该方法调用返回时,你也是不知道是否你所需要的数据是否在缓冲区(Buffer)中。你仅仅知道的事,缓冲区(Buffer)包含了些数据。这样让程序处理变得有些困难。

      如果在第一调用read(buffer)后,只有半行的数据被读入到缓冲区(Buffer)例如: "Name: An" ,你能处理这样的数据?显然不能,你必须等到至少一个完整行数据被读入到缓冲区,这样数据处理才有意义。

     你怎么能才能知道缓冲区里面已经完全包含了你所要处理的数据呢?好吧,你不能,唯一的方式检查缓冲区里的数据。这样导致的结果在所有数据数据在缓冲区里前,你必须检查几次在缓冲区里的数据。这不仅效率低下,而且可以使程序设计方案混乱不堪。例如:

ByteBuffer buffer = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buffer);

while(! bufferFull(bytesRead) ) {
    bytesRead = inChannel.read(buffer);
}

    bufferFull()方法必须跟踪多少数据被读入到缓冲区中,返回truefalse依赖的是缓冲区是否已经写满。

换句话说,一旦缓存区将被处理,可以认为缓冲区已经被写满。

    bufferFull()方法扫描缓冲区,必须保持在调用bufferFull()方法前后调用bufferFull()后一致的状态。如果不是,下个读入的数据将没有写入到正确的位置。这是不可能的,但它的另一个问题的引起注意。

     缓冲区满了,它能被处理。如果它没有满,在个别的案例中,你或许可以处理部分的数据。

     下图 说明 is-data-in-buffer-ready 循环:

      翻译-【Java NIO学习系列】Java NIO与IO

     总结

     NIO允许你使用一个(或多个)线程管理多个通道(network connections or files),解析数据的成功代价比从阻塞的流中读取数据的更大,更复杂。

     如果你要管理同时存在的数以千计的的连接,且只发送少量数据,例如:聊天服务,服务端NIO是一个最佳方案。同样,如果你要维持大量与其他计算机的连接,e.g P2P网络,使用单个线程管理外建的连接是个优势。单线程,多连接设计图示如下:
 翻译-【Java NIO学习系列】Java NIO与IO

如果你只有少许连接,并且有高带宽,在一个时间端内发送大量数据,或许标准IO是最佳方案。传统的IO设计图如下:
翻译-【Java NIO学习系列】Java NIO与IO

后面还有几篇文章,也正在翻译、、、、