Java-NIO之概述

  新的输入/输出 (NIO) 库是在JDK1.4中引入的,弥补了原IO的不足,叫做非阻塞IO。本篇博客主要讲解IO与NIO的区别以及NIO的基本概念。

为什么使用NIO

  NIO的创建目的是为了实现高速 I/O 而无需编写自定义的本机代码。NIO 将最耗时的 I/O 操作(即填充和提取缓冲区)转移回操作系统,因而可以极大地提高速度。I/O 与 NIO 最重要的区别是数据打包和传输的方式, I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。

  面向流 的 I/O 系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。一个 面向块 的 I/O 系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。

NIO概述

Java NIO 中Channel,Buffer 和 Selector 构成了NIO的核心的API。

  • Channel 和 Buffer
    基本上,所有的 IO 在 NIO 中都从一个Channel 开始。Channel 我们称之为通道, 数据可以从Channel通道中读到Buffer中,也可以从Buffer 写到Channel中。如图:

Channel 和 Buffer

Channel和Buffer有好几种类型。如果这些通道涵盖了UDP 和 TCP 网络IO,以及文件IO。

  1. FileChannel
  2. DatagramChannel
  3. SocketChannel
  4. ServerSocketChannel
  • Selector
    Selector允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。

Selector和Channel的关系

NIO与IO的区别

Java NIO和IO之间的主要差别:

  • IO: 面向流 阻塞IO 无
  • NIO: 面向缓冲 非阻塞IO 选择器
  1. 面向流与面向缓冲
    IO是面向流的,NIO是面向缓冲区的。Java IO 面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。Java NIO 处理过程灵活,数据读取到缓冲区,需要时可在缓冲区中前后移动,可对缓冲区的数据进行检测等。

  2. 阻塞与非阻塞IO
    Java IO 的各种流都是阻塞的。比如,当一个线程调用read()或write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
    Java NIO 非阻塞模式,比如,一个线程从某通道发送请求读取数据,它仅能得到目前可用的数据,如果目前没有数据可用时,该线程可以继续做其他的事情,直至有数据可读。 写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。

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

设计上的不同

API调用

Java NIO 的API调用并不是仅从一个InputStream逐字节读取,而是数据必须先读入缓冲区再处理。

数据处理

  • 在IO中,从InputStream或 Reader逐字节读取数据。如下列文本数据:
1
2
3
4
Name: xiaoxiaomo
Age: 24
Email: momo@xiaoxiaomo.com
Phone: 1234567890

该文本行的流可以这样处理:

1
2
3
4
5
6
7
8
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();

注意处理状态由程序执行多久决定。比如:String nameLine=reader.readLine()方法返回,表示该行已读完, readline()阻塞直到整行读完,这就是原因。你也知道此行包含名称;同样,第二个readline()调用返回的时候,你知道这行包含年龄等。即,该处理程序仅在有新数据读入时运行,并知道每步的数据是什么。一旦正在运行的线程已处理过读入的某些数据,该线程不会再回退进行数据。下图也说明了这条原则:

IO读取数据

  • 在NIO中的实现会有所不同,下面是一个简单的例子:
1
2
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = inChannel.read(buffer);

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

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

1
2
3
4
5
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = inChannel.read(buffer);
while(! bufferFull(bytesRead) ) {
bytesRead = inChannel.read(buffer);
}

bufferFull()方法必须跟踪有多少数据读入缓冲区,如果缓冲区准备好被处理,那么表示缓冲区满了。它可以被处理。如果它不满,并且在你的实际案例中有意义,你或许能处理其中的部分数据。但是许多情况下并非如此。下图展示了“缓冲区数据循环就绪”

NIO读取数据

线程数

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

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

一个线程多个连接

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

典型IO服务器设计

  • 参考资料
  1. http://tutorials.jenkov.com/java-nio/nio-vs-io.html
  2. http://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器