英文原文地址: 《Java NIO vs. IO》
当学习Java NIO和IO的API用法时,一个问题就从脑海里冒出:什么时候我们该用IO,什么时候我们该用NIO呢?在这篇文章中我会阐明Java 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作为你的IO工具包,都可能会在如下几个方面影响你的应用程序设计:
显然,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()方法返回时,你能知道这行包含了年龄。
正如你所看到的,只有当新的数据被读入时,程序才会继续往下走。并且你知道每一行读入的都是什么数据。一旦执行中的线程在读取代码中的特定数据之后取得进展,那么线程将不会在数据中倒退。原理如下图所示:
而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()方法调用前相同的状态。如果没有的话,接下来被读入到缓冲区的数据可能不会在正确的位置。这并不是不可能。但这是另一个需要注意的问题。
如果缓冲区满,那么它可以被处理。如果没满,你可以先处理部分已到达的数据。它有可能在你的特殊情况下有意义,但大多数情况下并非如此。
检测缓冲区是否就绪的图如下所示:
NIO允许你使用一个线程去管理多个通道(网络连接或者文件)。但代价是解析数据时可能会比阻塞的流读数据更加复杂。
如果你需要同时管理上千个连接时,每个连接仅发送一点数据,如聊天服务器。用NIO来实现这个服务器更有优势。同样的,如果你需要对其他计算机保持一堆连接,例如P2P网络,使用单个线程来管理所有连接可能是个优势。下图展示了单个线程多个连接的设计。
如果你有少量高带宽的链接,一次发送大量数据。可能经典的IO服务器实现会更适合。下图展示了经典IO服务器的设计:
转自我的个人博客 vc2x.com