在学习标准IO相关接口和NIO接口时,我们常常面临一个问题:什么时候应该使用IO接口?什么时候应该使用NIO接口?
这里我们将对比较Java NIO 和 IO的区别,他们的使用场景,如何使用他们编写高效的代码。
Java NIO和IO的主要区别
下边的表对比了二者的主要区别。
IO
- 基于Stream
- 阻塞
NIO
- 基于Buffer
- 非阻塞
- Selector支持
1.基于Stream VS 基于Buffer
Java NIO和IO的最大区别就是 IO是基于Stream的,而NIO是基于Buffer的,那这又意味着什么呢?
Java IO基于Stream意味着我们每次从Stream中读取一个或多个字节,我们如何处理读取到的字节取决于我们的需求,这些字节并不会缓存在任何地方。并且,我们不能在Stream的数据中前后来回移动;如果需要实现从流中读取数据中来回移动,就得首先将这些数据保存在一个缓存中。
Java NIO是基于Buffer的方式则有所不同,数据被读取到buffer中后再进行处理,我们可以根据需求在buffer中前后来回移动。这样在处理数据时提供了更多的灵活性。然而,我们也需要检查buffer是否包含我们需要处理的所有数据;我们还需要保证在读取更多数据的时候,新的数据不会覆盖buffer中未处理的数据。
2.阻塞 VS 非阻塞
Java IO的各种流Stream都是阻塞,这就意味着当一个线程执行read()或者write()方法的时候,线程会阻塞直到读到数据或者写入完成,这时线程无法做其他任何操作。
Java NIO的非阻塞模式则允许线程请求从Channel中读取数据时,只需要获取当前可读的数据或者当无数据可读时不读取任何数据就返回。相比于阻塞线程直到数据可读,线程可以做些其他操作。非阻塞写操作也是一样的,线程可以请求写入数据到一个Channel,但并不会阻塞等待数据全部写入,线程可以同时执行其他操作。
你可能会思考,当用非阻塞IO方法时,线程如何花费它的空闲时间,通常线程会去操作其他IO channel。这就是为什么一个单线程可以同时管理多个Channel处理输入输出。
3.Selectors
Java NIO 的Selector允许一个线程管理多个Channel,我们可以注册多个Channel到Selector中,然后使用一个线程去“select”可以处理的Channel。这种机制使得单线程管理多个Channel变得简单。
4.NIO 和 IO是如何影响应用程序设计的
当你选择NIO或者IO作为应用的数据交互工具时可能需要从以下几个方面进行考虑程序设计:
调用NIO或者IO类的接口
数据的处理流程
处理数据需要的线程数
调用接口
很明显使用NIO接口和IO的接口调用是不同的;不像从InputStream中逐个字节读取数据一样,数据需要被读取到buffer中进行后续处理。
数据处理流程
当使用单纯的NIO设计或者IO设计程序时,数据处理流程也会受到影响。
在IO设计中,数据逐个字节从InputStream或者Reader中读取,假设你处理向下边的基于行的文本数据流:
Name: Anna
Age: 25
Email: [email protected]
Phone: 1234567890
可能我们会像下边这样处理数据:
InputStream input = ... ; // 获取输入流
BufferedReader reader = new BufferedReader(new InputStreamReader(input)); // 穿件Reader读取字符
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)执行后,我们读取到的数据只是半行,例如“Name:An”,我们应该如何处理这些数据?我们需要等到至少一行数据读入到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必须重置bufferFull()调用前的状态;如果没有的话,下次数据读取可能读到错误的位置,这并不是不可能的,但这是另外需要考虑的问题。
当buffer完整的时候,它就可以被处理了。不完整的话你也可以处理部分数据,这在特殊场景下也是有意义的,大部分情况下是没有意义的。
下面是buffer读取数据的循环图:
5.总结
NIO允许我们使用单一线程管理多个channel(多个网络连接或者多个文件);但是相比从阻塞流中读取数据而言,NIO数据的处理会显得稍微复杂一些。
如果需要需要同时管理成千上万的链接,并且每个都发送少量的数据,例如聊天服务器;实现这样的服务器NIO显得更有优势一些。或者需要保持大量的链接到其他的计算机,例如P2P网络,使用一个线程管理所有的链接也比较有优势。
如果只有比较少的链接,但是需要很高的带宽每次发送大量的数据,标准IO来实现可能更合适。下图是一个标准IO服务器设计实例
转载自:冒烟儿
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。