Java NIO vs. IO

Java NIO, IO 大比拼,原文地址:http://tutorials.jenkov.com/java-nio/nio-vs-io.html

当学习 Java NIO 和 Java IO API 时,我们脑子中不禁会跳出一个问题:

什么时候我该使用 IO,什么时候我又该使用 NIO 呢?
接下来这篇文章我就会阐述 Java NIO 和 IO 之间的一些不同,它们的使用场景以及它们对你的代码设计的影响。

Java NIO 和 IO 之间的主要不同

下表列举了 Java NIO 和 IO 之间的主要不同:

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

面向流比拼面向缓冲区

Java NIO 和 IO 之间第一个重大不同点就是:Java NIO 是面向缓冲区的,而 IO 是面向流的。然而,这意味着什么呢?
面向流的 Java IO 意味着你一次性从一个流中读取一个或多个字节。你使用读取到的字节数据干什么取决于你自己,这些数据不再任何地方缓存。更进步一来说,你不能随意地在一个流数据中向前移动或向后移动。如果你想要这么做,你就得需要手动创建一个缓冲区缓存这些数据。
Java NIO 读取数据的方式和 IO 不同。它会先把需要处理的数据读取到一个缓冲区。然后你可以随意的向前移动或向后移动。这可以让你更加灵活的处理数据。然而,你必须得检查缓冲区是否包含了你所需要的所有数据。并且,当你需要读取更多数据时,你要确认不能覆盖你尚未处理的数据。

阻塞式 IO 和非阻塞式 IO

Java IO 中的几种流都是阻塞式的。那也就意味着:当使用一个线程调用 read() 或 write() 方法读写数据时,这个线程会一直阻塞直到可以读出一些数据或者所有的数据都已经写入完成了。在这期间,这个线程不能做任何事。
Java NIO 的非阻塞模式可以让一个线程从一个通道中读取数据,此时这个通道是可用的,要不然什么也不会做并直接返回。并不是一直阻塞直到数据可读,这个线程可以先去做其他事情。
对于写入数据时也是一样的。一个线程可以请求写入一些数据到一个通道,但并不比等待着所有数据写入完成。在这期间,这个线程可以去做其他的事情。
在非阻塞式 IO 调用时,一个线程空闲时间一般会检查其他通道。这也就说,一个线程现在可以管理多个通道的输入和输出了。

选择器

Java NIO 的选择器可让一个线程监控多个通道的输入。你可以把多个通道注册到一个选择器上,然后用一个线程来“选择”那些输入可用的通道以便进一步处理,或者选择那些可写的通道。使用选择器机制让一个线程管理多个通道变得非常容易。

NIO 和 IO 对应用设计的影响

选择 NIO 或 IO 作为 IO 工具包对你的应用设计有如下影响:

  1. 对 NIO 和 IO 类的 API 调用
  2. 数据的处理过程
  3. 处理数据时使用的线程数

API 的调用

当然了,使用 NIO 时调用的 API 和使用 IO 时的是不同的。这没什么奇怪的。不像你从一个输入流中读取数据那样,使用 NIO 你先得把数据读入一个缓冲区中,然后再处理它们。

数据的处理过程

使用 IO 时,你可以从输入流或读取器中读取数据。想象下你正在处理一个基于文本形式的流数据时:

1
2
3
4
Name: Anna
Age: 25
Email: anna@mailserver.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();

数据的处理状态取决于程序的运行过程。换句话说,一旦第一行的 reader.readLine() 方法返回了,你就可以确定已经读取了一整行的数据。因为方法 readLine() 会阻塞直到一整行数据被读取。你也可以知道这行的数据包含了名字。相似地,当第二行的 readLine() 方法返回时,你可以知道这行包含了年龄等。
正如你可以看到的,这个程序只有当有数据可读时才会执行下去,并且每一步你都可以知道自己读取的数据是什么。一旦进程读取了最后的数据,它也不会返回到读取数据那里(至少大多数是这样的)。这个概念就如下图所示:
Java IO: Reading data from a blocking stream.
一个非阻塞式的 NIO 实现看起来有点不同:

1
2
3
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);

但是第二行读取数据时并不能确保已经读取了一行数据到缓冲区中。所以你不得不一直检测这个缓冲区的数据是否已经足够一行:

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

对于 NIO 的整个过程就如下图所示:
Java NIO: Reading data from a channel until all needed data is in buffer.

总结

NIO 的适用场景:

  1. 有大量连接,但每个连接通信过程中每次只会发送少量数据(比如聊天服务器或者 P2P 网络)