Java NIO系列教程(十二) Java NIO与IO

原文地址:http://tutorials.jenkov.com/java-nio/nio-vs-io.html

作者:Jakob Jenkov   译者:郭蕾    校对:方腾飞

当学习了Java NIO和IO的API后,一个问题马上涌入脑海:

我应该何时使用IO,何时使用NIO呢?在本文中,我会尽量清晰地解析Java NIO和IO的差异、它们的使用场景,以及它们如何影响您的代码设计。

Java NIO和IO的主要区别

下表总结了Java NIO和IO之间的主要差别,我会更详细地描述表中每部分的差异。

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

面向流与面向缓冲

Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

阻塞与非阻塞IO

Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

选择器(Selectors

Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。

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

无论您选择IO或NIO工具箱,可能会影响您应用程序设计的以下几个方面:

  1.  对NIO或IO类的API调用。
  2. 数据处理。
  3. 用来处理数据的线程数。

API调用

当然,使用NIO的API调用时看起来与使用IO时有所不同,但这并不意外,因为并不是仅从一个InputStream逐字节读取,而是数据必须先读入缓冲区再处理。

数据处理

使用纯粹的NIO设计相较IO设计,数据处理也受到影响。

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

Name: Anna
Age: 25
Email: anna@mailserver.com
Phone: 1234567890

该文本行的流可以这样处理:
InputStream input = … ; // get the InputStream from the client socket

[code lang=”java”]
BufferedReader reader = new BufferedReader(new InputStreamReader(input));

String nameLine = reader.readLine();
String ageLine = reader.readLine();
String emailLine = reader.readLine();
String phoneLine = reader.readLine();
[/code]

请注意处理状态由程序执行多久决定。换句话说,一旦reader.readLine()方法返回,你就知道肯定文本行就已读完, readline()阻塞直到整行读完,这就是原因。你也知道此行包含名称;同样,第二个readline()调用返回的时候,你知道这行包含年龄等。 正如你可以看到,该处理程序仅在有新数据读入时运行,并知道每步的数据是什么。一旦正在运行的线程已处理过读入的某些数据,该线程不会再回退数据(大多如此)。下图也说明了这条原则: Java IO: 从一个阻塞的流中读数据) 而一个NIO的实现会有所不同,下面是一个简单的例子:

[code lang=”java”]
ByteBuffer buffer = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buffer);
[/code]

注意第二行,从通道读取字节到ByteBuffer。当这个方法调用返回时,你不知道你所需的所有数据是否在缓冲区内。你所知道的是,该缓冲区包含一些字节,这使得处理有点困难。
假设第一次 read(buffer)调用后,读入缓冲区的数据只有半行,例如,“Name:An”,你能处理数据吗?显然不能,需要等待,直到整行数据读入缓存,在此之前,对数据的任何处理毫无意义。

所以,你怎么知道是否该缓冲区包含足够的数据可以处理呢?好了,你不知道。发现的方法只能查看缓冲区中的数据。其结果是,在你知道所有数据都在缓冲区里之前,你必须检查几次缓冲区的数据。这不仅效率低下,而且可以使程序设计方案杂乱不堪。例如:

[code lang=”java”]
ByteBuffer buffer = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buffer);

while(! bufferFull(bytesRead) ) {

bytesRead = inChannel.read(buffer);

}
[/code]

bufferFull()方法必须跟踪有多少数据读入缓冲区,并返回真或假,这取决于缓冲区是否已满。换句话说,如果缓冲区准备好被处理,那么表示缓冲区满了。

bufferFull()方法扫描缓冲区,但必须保持在bufferFull()方法被调用之前状态相同。如果没有,下一个读入缓冲区的数据可能无法读到正确的位置。这是不可能的,但却是需要注意的又一问题。

如果缓冲区已满,它可以被处理。如果它不满,并且在你的实际案例中有意义,你或许能处理其中的部分数据。但是许多情况下并非如此。下图展示了“缓冲区数据循环就绪”:

Java NIO:从一个通道里读数据,直到所有的数据都读到缓冲区里.

3) 用来处理数据的线程数

NIO可让您只使用一个(或几个)单线程管理多个通道(网络连接或文件),但付出的代价是解析数据可能会比从一个阻塞流中读取数据更复杂。

如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器,实现NIO的服务器可能是一个优势。同样,如果你需要维持许多打开的连接到其他计算机上,如P2P网络中,使用一个单独的线程来管理你所有出站连接,可能是一个优势。一个线程多个连接的设计方案如下图所示:

Java NIO: 单线程管理多个连接

如果你有少量的连接使用非常高的带宽,一次发送大量的数据,也许典型的IO服务器实现可能非常契合。下图说明了一个典型的IO服务器设计:

Java IO: 一个典型的IO服务器设计- 一个连接通过一个线程处理.

原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: Java NIO系列教程(十二) Java NIO与IO

  • Trackback 关闭
  • 评论 (39)
  1. 赞一个 写得不错!

    • ℡actionフ
    • 2013/05/07 4:38下午

    下面的两个图有什么区别

      • 匿名
      • 2013/11/16 4:56下午

      同问

        • gongweixin
        • 2014/02/27 5:48下午

        图画的不是太好 ,上面的图是说 多个connection 对应一个thread ,下面的图是说 每个connection 对应一个thread , 根据我的理解图这么画会好点、
        |——————|
        | Thread |
        |————-| |——————|
        | Thread |———————->| |————| |
        |————-| | | Connection | |
        |ServerSocket | | |————| |
        |————-| | | Connection | |
        | |————| |
        |——————|

        ====================================================================
        |——————|
        | Thread |
        |————-| |——————|
        | Thread |———————->| Connection |
        |————-| |——————|
        |ServerSocket |
        |————-|———————->|——————|
        | Thread |
        |——————|
        | Connection |
        |——————|

        • gongweixin
        • 2014/02/27 5:49下午

        坑啊, 好不容易画好的 发表之后就乱了。

      • 截图吧

        • gongweixin
        • 2014/02/27 5:55下午

        图画的不是太好 ,上面的图是说 多个connection 对应一个thread ,下面的图是说 每个connection 对应一个thread , 根据我的理解图这么画会好点、
        |==================|
        | Thread |
        |=============| |==================|
        | Thread |———————->| |============| |
        |=============| | | Connection | |
        |ServerSocket | | |============| |
        |=============| | | Connection | |
        | |============| |
        |==================|
        +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
        |==================|
        | Thread |
        |=============| |==================|
        | Thread |======================>| Connection |
        |=============| |==================|
        |ServerSocket |
        |=============|======================>|==================|
        | Thread |
        |==================|
        | Connection |
        |==================|

  2. hi, 读完了关于 nio 的文章。 请问有没有相关的全面点的 demo 能更深刻的理解?

  3. is-data-in-buffer-ready loop 翻译成“缓冲区数据循环就绪”,合适吗?

    • gongweixin
    • 2014/02/27 5:56下午

    夕水溪下 :
    截图吧

    不会啊 、

    • 夏末冬初
    • 2014/03/14 6:39下午

    Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。

    没有数据可读之前做别的事情,这个别的事情泛指哪些事情,假如我这个线程就需要读出的所有数据,然后进行打印,那这时候打印出来的是部分数据,还是所有数据。

      • 木槿花萧
      • 2014/08/31 3:42下午

      你可以写个回调或者Future来把数据读取完之后的处理代码放进去,然后可以不等待数据处理完就可以做任何事情,比如继续监听客户端请求之类的。 但总感觉NIO是以增加了数据处理复杂度来换取cpu的时间。

    • 夏末冬初
    • 2014/03/14 7:07下午

    ByteBuffer buffer = ByteBuffer.allocate(48);
    int bytesRead = inChannel.read(buffer);
    while(! bufferFull(bytesRead) ) {
    bytesRead = inChannel.read(buffer);
    }

    一定要这样不断的轮训检查吗,这样效率貌似和阻塞io没有啥区别啊。而且什么时候判断数据是完整的呢?

      • 木槿花萧
      • 2014/08/31 3:54下午

      int bytesRead = inChannel.read(buffer); 这里返回的字节数是-1的时候证明你已经读完了,前几篇文章有写。至于有没有读取到完整数据需要你自己的代码控制好。第三章通道的示例里面有写:http://ifeve.com/channels/

  4. 为什么NIO是同步非阻塞的? 同步是哪里同步?阻塞是哪里阻塞?

    • NIO的应用场景:多连接小数据。你可以这样认为,线程处理完某一连接的一部分数据之后,下一部分数据还没到,这样,线程有时间先去处理另一个连接的数据。楼主的例子中没有体现多连接的情况。

      • terryg
      • 2017/01/17 11:48下午

      NIO的同步应该是说的NIO的底层实现,有同步非阻塞和异步非阻塞两种实现方式,参见:http://www.artima.com/articles/io_design_patterns.html

    • 在路上
    • 2014/09/30 11:48上午

    读完整个系列,对Nio有了个大概的了解,十分感谢!

  5. 读完整个系列,对NIO有大致的了解了,非常感谢!但是,请问,有没有一个比较完善点的例子,能体现NIO在实际工作中的运用吗?目前,对NIO的优势持疑惑状态,不能体会它的优点,望解答~

    • 可以对比使用下Java IO ,使用起来NIO比较方便,有更多更高级的API,比如transferTo三行代码就能实现从一个文件读取数据到另外一个文件。

  6. nio既然是异步的, 那有没有回调函数或者defer函数呢?

    • '4 ever love
    • 2014/11/06 5:18下午

    bufferFull()方法扫描缓冲区,但必须保持在bufferFull()方法被调用之前状态相同。如果没有,下一个读入缓冲区的数据可能无法读到正确的位置。这是不可能的,但却是需要注意的又一问题。

    这一段翻译有问题,应该搞成:

    通过bufferFull()方法扫描缓冲区,缓冲区必须保持和bufferFull()方法被调用之前相同的状态,如果不是,下一个读入缓冲区的数据可能无法被读到正确的位置,这并非不可能,但也暴露出另一个需要我们注意的问题

    • hequn
    • 2015/01/06 3:07下午

    “这是不可能的,但却是需要注意的又一问题。” 应该是“这不是不可能的,但却是需要注意的又一问题。”原文This is not impossible, but it is yet another issue to watch out for.

    • 雨蓝
    • 2015/04/25 5:35下午

    nio处理多而小的连接,用了缓冲区,那么会不会时时性不好了呢?

    • jk1420
    • 2015/06/09 5:00下午

    图片都坏了,看不到

      • suc
      • 2015/09/06 6:45下午

      点进原文链接可以看到

    • sstong123
    • 2015/07/06 10:21上午

    好文章,但文中图片都不能显示,楼主可不可以补一下?

    • hl174
    • 2016/04/26 4:02下午

    bufferFull

    怎么没有发现这个方法

    • hl174
    • 2016/04/26 4:07下午

    话说仔细看了这一些列12期的nio文章,还是不知道具体怎么用,有没有深点的应用型的文章

    • jzh0535
    • 2016/06/27 7:20下午

    mark!

    • Yancy
    • 2016/09/16 10:25上午

    图加载不了

    • bw
    • 2016/11/03 1:39下午

    这些图怎么都不显示啦?

    • lin_1220
    • 2017/01/12 10:57上午

    楼主,您好,里面的图全都看不到

    • adeline
    • 2017/12/22 4:23下午

    看不到图

    • red_and_black
    • 2018/06/07 11:11上午

    楼主,哪里有图啊,没有图啊,原文中倒是有图

    • menfre
    • 2018/09/15 2:46下午

    “bufferFull()方法扫描缓冲区,但必须保持在bufferFull()方法被调用之前状态相同。如果没有,下一个读入缓冲区的数据可能无法读到正确的位置。这是不可能的,但却是需要注意的又一问题。”

    这里是说保持bufferFull()调用前后的缓冲区状态(position/limit)是不可能的吗? 可以通过检查前 mark position的位置,在检查完成通过reset来保证检查前后缓冲区的状态一致。

    • Arry0624
    • 2019/01/11 4:54下午

    QAQ~学习学习

  7. 这个翻译,,有点强行了

    • 放腾飞的粉丝
    • 2021/07/16 3:31下午

    图没有了大佬

return top