Solo  当前访客:0 开始使用

CoderV的进阶笔记


Everybody wants go to heaven, but nobody wants to die.

[译]Java NIO vs. IO

2019-12-17 09:00:45 valarchie
0  评论    0  浏览

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

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

NIO和IO主要的不同点

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

IONIO
面向流面向缓冲区(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 NIO vs. IO
作者:valarchie
地址:http://vc2x.com/articles/2019/12/17/1576513907078.html
技术讨论群:1398880
本文为博主原创文章,转载请附上原文出处链接。
,
TOP