NIO
1. 基础概念
在NIO中有几个比较关键的概念:Channel(通道),Buffer(缓冲区),Selector(选择器)。下例是传统的IO中读取一个文件内容的方式:
- 这里的InputStream实际上就是为读取文件提供一个通道。因此可以将NIO 中的Channel同传统IO中的Stream来类比,但在传统IO中,Stream是单向的,比如InputStream只能进行读取操作,OutputStream只能进行写操作。而Channel是双向的,既可用来进行读操作,又可用来进行写操作。
- Buffer(缓冲区)是NIO中非常重要的一个概念,在NIO中所有数据的读和写都离不开Buffer。比如上面的一段代码中,读取的数据是放在byte数组当中,而在NIO中,读取的数据只能放在Buffer中。同样地,写入数据也是先写入到Buffer中。
- NIO中最核心的一个东西是Selector,Selector的作用就是用来轮询每个注册的Channel,一旦发现Channel有注册的事件发生,便获取事件然后进行处理。
用单线程处理一个Selector,然后通过Selector.select()方法来获取到达事件,在获取了到达事件之后,就可以逐个地对这些事件进行响应处理。
2. Channel
Channel和传统IO中的Stream很相似,但也有很大的区别,主要区别为:通道是双向的,通过一个Channel既可以进行读,也可以进行写;而Stream只能进行单向操作,通过一个Stream只能进行读或者写。以下是常用的几种通道,这些通道涵盖了UDP 和 TCP 网络IO,以及文件IO:
- FileChannel:通过使用FileChannel可以从文件读或向文件写入数据;
- SocketChanel:通过SocketChannel,以TCP来向网络连接的两端读写数据;
- ServerSocketChannel:通过ServerSocketChanel能够监听客户端发起的TCP连接,并为每个TCP连接创建一个新的SocketChannel来进行数据读写;
- DatagramChannel:通过DatagramChannel,以UDP协议来向网络连接的两端读写数据。
下面实例为通过FileChannel向文件中写入数据:
运行结果:
3. Buffer
Buffer(缓冲区)实际上是一个容器。Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer。在NIO中,Buffer是一个顶层父类,它是一个抽象类,常用的Buffer的子类有:
- ByteBuffer
- IntBuffer
- CharBuffer
- LongBuffer
- DoubleBuffer
- FloatBuffer
- ShortBuffer
这些Buffer覆盖了能通过IO发送的基本数据类型:byte, short, int, long, float, double 和 char。如果是对于文件读写,上面几种Buffer都可能会用到,对于网络读写来说,用的最多的是ByteBuffer。
Buffer通过几个变量来保存这个数据的当前位置状态,也就是有四个索引:
- ByteBuffer.allocate(11)创建了一个11个byte的数组缓冲区,初始状态如下:position的位置为0,capacity和limit默认为数组长度。
- 当写入5个字节时,位置变化如下图所示:
- 这时需要将缓冲区的5个字节数据写入channel通信信道,调用byteBuffer.flip()方法,位置变化如下:
- 下一次写数据之前调用clear()方法即可
4. Selector
Selector允许单线程处理多个 Channel。Selector能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样一来,只用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。与Selector有关的一个关键类是SelectionKey,一个SelectionKey表示一个到达的事件,这2个类构成了服务端处理业务的关键逻辑。
5. 与传统IO的区别
- 面向流与面向缓冲:Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动流中的数据,需要先将它缓存到一个缓冲区。Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
- 阻塞与非阻塞IO:Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程会被阻塞直到有数据被读取,或数据完全写入。该线程在此期间不能再干任何事情。Java NIO的非阻塞读模式,使一个线程从某通道发送请求读取数据,它仅能读到目前可用的数据,如果目前没有数据可用时,不会阻塞,直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
- 选择器:Java NIO的选择器允许一个单独的线程来监视多个输入通道,使用一个单独的线程来选择通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。