NIO vs. IO

在学习Java NIO和IO时,一个问题很快浮现在脑海里:

我应该嘛时候用IO,又嘛时候用NIO呢?

在本文中,我将试图阐明Java NIO和IO的不同,它们的使用场景,以及它们如何影响你的代码设计。

Main Differences Betwen Java NIO and IO

下面的表格总结了Java NIO和IO的主要不同点。我会在表格后面的章节中详细阐述每一个不同点。

IO NIO
面向流的 面向Buffer的
阻塞IO 非阻塞IO
Selectors

Stream Oriented vs. Buffer Oriented

Java NIO和IO的第一大不同就是IO是面向流的,而NIO是面向Buffer的。那么,这意味着什么呢?

Java IO面向流意味着你每次从流中读取一个或多个字节。至于这么处理读取出来的字节就取决于你个人了。这些数据不会被任何东西缓存起来。进一步说,你不能在流中进行前后的移动。如果你想在从流中读取数据时候前后移动,那么你需要先先将数据用个Buffer缓存起来。

Java NIO的面向Buffer的方法有些稍微的不同。数据被读入稍后进行处理的Buffer中。你可以在Buffer中进行前后的移动,如果你需要的话。这给你在处理数据时提供了更高的灵活性。但是,在完整的处理数据之前,你还是需要检查Buffer中时候已经包含了所有你需要的数据。并且,你还要确保在往Buffer中读入更多数据的时候,不会覆盖那些还未被处理的数据。

Blocking vs. Non-blocking IO

Java IO中的各种流都是阻塞的。也就是说,当一个线程调用read()write()方法的时候,线程会一直阻塞,直到真正读取到数据,或者全部数据都被写入。该线程在同一时间不能再去干其他任何事。

Java NIO的非阻塞模式允许线程请求从Channel读取数据,并且只能读到当前可用的数据,或者什么也读不到,如果当前没有可用数据的话。不用一直阻塞到有数据可读为止,线程可以去做其他事情。

非阻塞的写也一样。线程可以请求向Channel写入数据,但不用一直等到数据全部写入为止。它可以继续执行其他操作。

当线程的空闲时间没有被IO操作阻塞时,一般都花在了其他Channel的IO上。也就是说,一个线程现在可以管理多个Channel的输入和输出了。

Selectors

Java NIO的Selector允许一个线程监控多个Channel的输入。你可以向Selecotr注册多个Channel,然后只用一个线程来“select”那些已经有输入数据可以处理的Channel,或者“select”那些可以写入数据的Channel。这种Selector机制使单一线程管理多个Channel变得简单。

How NIO and IO Influences Application Design

你选择NIO还是IO来作为你的IO工具包,会在以下几个方面影响你的应用程序设计:

  1. 调用NIO和IO的API类;
  2. 数据的处理;
  3. 用来处理数据的线程数。

The API Calls

当然,调用NIO的API和调用IO的API看起来肯定不一样。这没啥好惊讶的。比起直接从一个比如InputStream直接读取字节数据,使用NIO必须先将数据读入一个Buffer,然后再处理Buffer里面的数据。

The Processing of Data

使用纯的NIO设计或传统IO设计,对数据的处理也有影响。在IO设计中,你从InputStream或者Reader读取字节数据。想象你正在处理一个基于行的文本流,比如:

Name: Anna
Age: 25
Email: [email protected]
Phone: 1234567890

这个文本流可能被这样处理:

InputStream input = ... ; // 从客户端获取流

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 vs. IO_第1张图片
Java IO:从阻塞型流中读取数据

NIO的实现会看起来有些不一样。这里有一个简单的例子:

ByteBuffer buffer = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buffer);

注意第二行,从Channel读取数据到Buffer中。当这个方法返回的时候,你并不知道你需要的数据是否已经都在Buffer中了。你知道的只是这个Buffer里有数据。这让数据处理变得有点复杂了。

想象一下,假如,第一次调用read(buffer)后,Buffer里面只有半行的数据。比如,“Name: An”。你能处理这个数据么?不见得能。你需要等到至少一行的数据已经完整的读入Buffer,这样的数据才有处理的意义。

那么,你怎么知道Buffer里包含的数据已经可以进行处理了?好吧,你并不知道。唯一的办法就是去看Buffer里面的数据。结果就是,在你知道Buffer里面已经有足够的数据之前,你可能必须对Buffer里面的数据进行多次检查。这不仅效率低,而且可能使程序设计变得混乱。比如:

ByteBuffer buffer = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buffer);

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

bufferFull()方法必须跟踪记录有多少数据已被读入Buffer,并且根据Buffer里的数据是否已满truefalse。换句话说,如果Buffer里的数据已经够处理了,则认为是满了。

bufferFull()方法会对Buffer进行扫描,但扫描结束后Buffer的状态必须和扫描前一样。如果不一样,后续读入Buffer的数据可能会被放到错的位置。这不是不可能,但却是另一个需要注意的问题。

如果Buffer满了,那就可以被处理了。如果还没满,也也可以做部分处理,如果这对你来说有意义的话。但多数情况下都不行。

检查Buffer里的数据是否足够做处理的循环如下图所示:

NIO vs. IO_第2张图片
Java NIO:从Channel读取数据至所需数据都已读入Buffer

Summary

NIO允许你只使用一个(或少量的)线程管理多个Channel(网络连接或文件),但代价是解析数据比直接从阻塞流读取数据要复杂一些。

如果你需要同时管理成千上万个连接,每个连接只发送少量数据,比如聊天服务器,那么使用NIO来实现可能有优势些。同样的,如果你需要保持很多连接到其他服务器的连接,比如P2P网络,使用单一线程来管理这些出站连接可能更好。这种单线程,多连接的设计如下图所示:
NIO vs. IO_第3张图片
Java NIO:单线程管理多连接

如果你有一些占用很大带宽的连接,每次发送很多数据,可能传统IO是最好的实现方式。传统IO服务器设计如下图所示:
NIO vs. IO_第4张图片
Java IO:传统IO服务器设计 - 一个线程负责一个连接

发现貌似有人在看这个系列文章了,有必要说明下,这个Java NIO系列来源于jenkov.com,本文只是翻译,希望大家千万不要误会,本文不是原创。原文地址:Java NIO。

你可能感兴趣的:(NIO vs. IO)