在学习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工具包,会在以下几个方面影响你的应用程序设计:
- 调用NIO和IO的API类;
- 数据的处理;
- 用来处理数据的线程数。
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的实现会看起来有些不一样。这里有一个简单的例子:
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
里的数据是否已满true
或false
。换句话说,如果Buffer
里的数据已经够处理了,则认为是满了。
bufferFull()
方法会对Buffer
进行扫描,但扫描结束后Buffer
的状态必须和扫描前一样。如果不一样,后续读入Buffer
的数据可能会被放到错的位置。这不是不可能,但却是另一个需要注意的问题。
如果Buffer
满了,那就可以被处理了。如果还没满,也也可以做部分处理,如果这对你来说有意义的话。但多数情况下都不行。
检查Buffer
里的数据是否足够做处理的循环如下图所示:
Summary
NIO允许你只使用一个(或少量的)线程管理多个Channel
(网络连接或文件),但代价是解析数据比直接从阻塞流读取数据要复杂一些。
发现貌似有人在看这个系列文章了,有必要说明下,这个Java NIO系列来源于jenkov.com,本文只是翻译,希望大家千万不要误会,本文不是原创。原文地址:Java NIO。