[译]Java NIO vs. IO

本贴最后更新于 1832 天前,其中的信息可能已经事过景迁

英文原文地址:《Java NIO vs. IO》

当学习 Java NIO 和 IO 的 API 用法时,一个问题就从脑海里冒出:什么时候我们该用 IO,什么时候我们该用 NIO 呢?在这篇文章中我会阐明 Java NIO 和 IO 之间的不同点和使用场景以及它们是如何影响代码的设计。

NIO 和 IO 主要的不同点

以下的表格总结了 NIO 和 IO 之间主要的不同点。我会以更多的细节去阐明这些不同点在接下来的几节中。

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

面向流与面向缓冲区

第一个不同点在于 IO 是面向流的而 NIO 是面向缓冲区的。所以这意味着什么?

Java IO 是面向流的意味着你从一个流中一次读取一个或者多个字节。如何处理读出的字节取决于你。它们不会缓存到任何地方。此外你不能在流中来回移动数据。如果你需要来回移动从流中读取的数据,你需要将其先缓存到一个缓冲区中。

Java 面向缓冲区的方法稍微有点不同。数据先被读入到一个缓冲区,然后再处理。你也可以将你的数据在缓冲区中来回移动。这使得你在处理过程中多了一些灵活性。但是,你必须检查缓冲区内是否包含了完整处理过程所需的所有数据。并且你必须确保当读入更多数据到缓冲区时,不能覆盖尚未处理的数据。

阻塞与非阻塞

Java IO 中的各种流都是阻塞的。这意味着当一个线程调用了 read()或者 write()方法时,这个线程会被一直阻塞直到数据被读入或被完全写入。线程在此期间不能做任何事情。

Java NIO 的非阻塞模式允许线程从一个通道中读取数据,并且只获取当就绪的数据,或者什么也不获取。如果当前没有就绪的数据,线程可以先去做其他的事情,而不是一直阻塞直到需要读入的数据就绪为止。

对于非阻塞的写也是如此。线程可以请求将数据写入到通道,但无需等待数据被完全写入。与此同时,线程可以继续执行做一些其他的事。

当线程在没有 IO 阻塞的空闲时间里主要做什么呢?它通常会同时在其他的通道上执行 IO 操作。也就是说一个线程现在可以管理多个输入输出通道。

选择器

Java NIO 是选择器允许一个线程去监视多个输入的通道。你可以将多个通道注册到一个选择器上。然后使用一个线程去选择可处理的输入通道,或者选择可写入的通道。这个选择器的机制使得线程管理多个通道变得简单。

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

无论你选择 NIO 还是 IO 作为你的 IO 工具包,都可能会在如下几个方面影响你的应用程序设计:

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

API 的调用

显然,API 调用 NIO 是不同于 IO 的,这不足为奇。与其从 InputStream 中一个字节一个字节的读取数据,不如先将数据读取到缓冲区,然后再从缓冲区开始处理。

数据的处理

与 IO 设计相比,当使用纯 NIO 的设计时会影响数据的处理。

在 IO 设计中,我们从 InputStream 或者 Reader 中一个字节一个字节的读取数据。假设你当前正在处理一个基于行的文本数据流。如下所示:

Name: Anna
Age: 25
Email: anna@mailserver.com
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()方法返回时,你能肯定一整行的文本已经被读入。readLine()方法会被阻塞直至一整行文本被读入。这也是为什么你能够知道这行包含了名字。同样的,当第二行 readLine()方法返回时,你能知道这行包含了年龄。

正如你所看到的,只有当新的数据被读入时,程序才会继续往下走。并且你知道每一行读入的都是什么数据。一旦执行中的线程在读取代码中的特定数据之后取得进展,那么线程将不会在数据中倒退。原理如下图所示:

niovsio1.png

而 NIO 的实现看起来并不同。简单的例子如下:

ByteBuffer buffer = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buffer);

注意到第二行将字节从通道中读取到缓冲区内。当这个方法调用返回时,你不知道你所需的所有数据是否已经都在缓冲区中。你能知道的只是该缓冲区中包含了一些字节。这使得处理过程有些困难。

假设,在第一个 read(buffer)调用时,读入到缓冲区的只是半行。如:“name:An”,你能处理这个数据吗?并不能,在处理任何数据之前,你必须等待直到至少一整行的数据已被读入到缓冲区中。

所以你如何知道缓冲区中已包含可被处理的足够数据。对,你并不能。唯一的办法是去观察缓冲区中的数据。结果是当你知道缓冲区中的数据已完全就绪之前,你可能会对缓冲区中的数据观测好几次。这两个都是低效的,并使得程序设计方面变得混乱。举例如下:

ByteBuffer buffer = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buffer);

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

bufferFull()方法必须追踪缓冲区内有多少数据,并取决于缓冲区是否满返回 true 或 false。换句话说就是当缓冲区已准备好进行处理,那么它被认为是满的。

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

如果缓冲区满,那么它可以被处理。如果没满,你可以先处理部分已到达的数据。它有可能在你的特殊情况下有意义,但大多数情况下并非如此。

检测缓冲区是否就绪的图如下所示:

niovsio2.png

总结

NIO 允许你使用一个线程去管理多个通道(网络连接或者文件)。但代价是解析数据时可能会比阻塞的流读数据更加复杂。

如果你需要同时管理上千个连接时,每个连接仅发送一点数据,如聊天服务器。用 NIO 来实现这个服务器更有优势。同样的,如果你需要对其他计算机保持一堆连接,例如 P2P 网络,使用单个线程来管理所有连接可能是个优势。下图展示了单个线程多个连接的设计。

niovsio3.png

如果你有少量高带宽的链接,一次发送大量数据。可能经典的 IO 服务器实现会更适合。下图展示了经典 IO 服务器的设计:

niovsio4.png

笔者个人翻译,如有错误恳请网友评论指正。

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3190 引用 • 8214 回帖 • 1 关注
  • 程序员

    程序员是从事程序开发、程序维护的专业人员。

    574 引用 • 3533 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...