Java-NIO之概述
新的输入/输出 (NIO) 库
是在JDK1.4中引入的,弥补了原IO的不足,叫做非阻塞IO
。本篇博客主要讲解IO与NIO的区别以及NIO的基本概念。
为什么使用NIO
NIO的创建目的是为了实现高速 I/O 而无需编写自定义的本机代码。NIO 将最耗时的 I/O 操作(即填充和提取缓冲区)转移回操作系统,因而可以极大地提高速度。I/O 与 NIO 最重要的区别是数据打包和传输的方式, I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。
面向流 的 I/O 系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。一个 面向块 的 I/O 系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。
NIO概述
在 Java NIO 中Channel,Buffer 和 Selector 构成了NIO的核心的API。
- Channel 和 Buffer
基本上,所有的 IO 在 NIO 中都从一个Channel 开始。Channel 我们称之为通道, 数据可以从Channel通道中读到Buffer中,也可以从Buffer 写到Channel中。如图:
Channel和Buffer有好几种类型。如果这些通道涵盖了UDP 和 TCP 网络IO,以及文件IO。
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
- Selector
Selector允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。
NIO与IO的区别
Java NIO和IO之间的主要差别:
- IO: 面向流 阻塞IO 无
- NIO: 面向缓冲 非阻塞IO 选择器
面向流与面向缓冲
IO是面向流的,NIO是面向缓冲区的。Java IO 面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。Java NIO 处理过程灵活,数据读取到缓冲区,需要时可在缓冲区中前后移动,可对缓冲区的数据进行检测等。阻塞与非阻塞IO
Java IO 的各种流都是阻塞的。比如,当一个线程调用read()或write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
Java NIO 非阻塞模式,比如,一个线程从某通道发送请求读取数据,它仅能得到目前可用的数据,如果目前没有数据可用时,该线程可以继续做其他的事情,直至有数据可读。 写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。选择器(Selectors)
Java NIO的选择器 允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
设计上的不同
API调用
Java NIO 的API调用并不是仅从一个InputStream逐字节读取,而是数据必须先读入缓冲区再处理。
数据处理
- 在IO中,从InputStream或 Reader逐字节读取数据。如下列文本数据:
1 | Name: xiaoxiaomo |
该文本行的流可以这样处理:
1 | InputStream input = ... ; // get the InputStream from the client socket |
注意处理状态由程序执行多久决定。比如:String nameLine=reader.readLine()方法返回,表示该行已读完, readline()阻塞直到整行读完,这就是原因。你也知道此行包含名称;同样,第二个readline()调用返回的时候,你知道这行包含年龄等。即,该处理程序仅在有新数据读入时运行,并知道每步的数据是什么。一旦正在运行的线程已处理过读入的某些数据,该线程不会再回退进行数据。下图也说明了这条原则:
- 在NIO中的实现会有所不同,下面是一个简单的例子:
1 | ByteBuffer buffer = ByteBuffer.allocate(1024); |
注意第二行,从通道读取字节到ByteBuffer。当这个方法返回时,你不知道你所需的所有数据是否在缓冲区内。你所知道的是,该缓冲区包含一些字节,这使得处理有点困难。
假设第一次 read(buffer)调用后,读入缓冲区的数据只有半行,例如,“Name:An”,你能处理数据吗?显然不能,需要等待,直到整行数据读入缓存,在此之前,对数据的任何处理毫无意义。
所以,你怎么知道是否该缓冲区包含足够的数据可以处理呢?好了,你不知道。发现的方法只能查看缓冲区中的数据。其结果是,在你知道所有数据都在缓冲区里之前,你必须检查几次缓冲区的数据。这不仅效率低下,而且可以使程序设计方案杂乱不堪。例如:
1 | ByteBuffer buffer = ByteBuffer.allocate(1024); |
bufferFull()方法必须跟踪有多少数据读入缓冲区,如果缓冲区准备好被处理,那么表示缓冲区满了。它可以被处理。如果它不满,并且在你的实际案例中有意义,你或许能处理其中的部分数据。但是许多情况下并非如此。下图展示了“缓冲区数据循环就绪”
线程数
NIO可让您只使用一个(或几个)单线程管理多个通道(网络连接或文件),但付出的代价是解析数据可能会比从一个阻塞流中读取数据更复杂。
如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器,实现NIO的服务器可能是一个优势。同样,如果你需要维持许多打开的连接到其他计算机上,如P2P网络中,使用一个单独的线程来管理你所有出站连接,可能是一个优势。一个线程多个连接的设计方案如下图所示:
如果你有少量的连接使用非常高的带宽,一次发送大量的数据,也许典型的IO服务器实现可能非常契合。下图说明了一个典型的IO服务器设计:
- 参考资料