1 NIO简介
在1.4版本之前,Java NIO类库是阻塞IO,从1.4版本开始,引进了新的异步IO库,被称为Java New IO类库,简称为Java NIO。New IO类库的目的 就是要让Java支持非阻塞IO。
Java NIO类库包含三个核心组件:
1、Channel(通道)
2、Buffer(缓冲区)
3、Selector(选择器)
理解了上一章高并发IO底层原理,大家会马上识别出来Java NIO属于第三种模型——IO多路复用模型。只不过,Java NIO组件提供了统一的API,为大家屏蔽了底层的操作系统的差异。
本章,会对上面3个组件展开详细介绍。首先看一下Java NIO和OIO的简单对比。
1.1 NIO和OIO的对比
在Java中,NIO和OIO的区别主要体现在三个地方:
1、OIO是面向流的,NIO是面向缓冲区的。在一般的OIO操作中,面向字节流或字符流的IO操作总是以流式的方式顺序地从一个流(Stream)中读取一个或多个字节,因此,我们不能随意改变读取指针的位置。在NIO操作中则不同,NIO中引入了Channel和Buffer的概念。面向缓冲区的读取和写入只需要从通道读取数据到缓冲区中,或将数据从缓冲区写入到通道中。NIO不像OIO那样顺序操作,它可以随意读取Buffer中任意位置的数据。
2、OIO的操作是阻塞的,而NIO是非阻塞的。OIO操作时,例如调用一个read方法读取一个文件的内容,调用read的线程就会被阻塞,直到read操作完成。在NIO模式中,当调用read方法时,如果此时有数据,则read读取数据并返回,如果此时没有数据,则read也返回直接返回,而不会阻塞当前线程。
3、OIO没有选择器(Selector)的概念,而NIO有选择器的概念。NIO的实现是基于底层选择器的系统调用的,所以NIO需要底层操作系统提供支持,而OIO不需要选择器。
1.2 通道
在OIO中,同一个网络连接会关联两个流:一个是输入流(Input Stream),另一个是输出流(Output Stream)。Java应用程序通过这两个流不断地进行输入和输出的操作。
在NIO中,一个网络连接使用一个通道表示,所有NIO的IO操作都是通过连接通道完成的。一个通道类似于OIO中两个流的结合体,既可以从通道读取数据,也可以向通道写入数据。
1.3 选择器
首先回顾一下前面介绍的基础知识——IO多路复用(高并发IO底层原理)指的是一个进程/线程可以同时监视多个文件描述符(含socket连接),一旦其中的一个或多个文件描述符可读或者可写,该监听进程/线程就能够进行IO就绪事件的查询。
在Java应用层面,如何实现对多个文件描述符的监视呢?需要用到一个非常重要的Java NIO组件——选择器。选择器可以理解为一个IO事件的监听与查询器。通过选择器,一个线程可以查询多个通道的IO事件的就绪状态。
从编程实现维度来说,IO多路复用变成的第一步是把通道注册到选择器中,第二步是通过选择器所提供的事件查询(select)方法来查询这些注册的通道是否有已经就绪的IO事件(例如可读、可写、网络连接完成等)。
由于一个选择器只需要一个线程进行监控,因此我们可以很简单地使用一个线程,通过选择器去管理多个连接通道。
与OIO相比,NIO使用选择器的最大优势就是系统开销小。系统不必为每一个网络连接(文件描述符)创建进程/线程,从而大大减少了系统的开销。总之,一个线程负责多个连接通道的IO处理是非常高效的,这种高效来自Java的选择器组件Selector及其底层的操作系统IO多路复用技术的支持。
1.4 缓冲区
应用程序与通道的交互主要是进行数据的读取和写入。为了完成NIO的非阻塞读写操作,NIO准备了第三个重要的组件——Buffer。所谓通道的读取,就是将数据从通道读取到缓冲区中;通道的写入,就是将数据从缓冲区写入通道中。缓冲区的使用是面向流进行读写操作的OIO所没有的,也就是NIO非阻塞的重要前提和基础之一。
2 Buffer类
Buffer类是一个抽象类,对应于Java的主要数据类型。在NIO中,有8种缓冲区类,分别是ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer、MappedByteBuffer。前7种Buffer类型覆盖了能在IO中传输的所有Java基本数据类型,第8种类型是一种专门用于内存映射的ByteBuffer类型。不同的Buffer子类可以操作的数据类型能够通过名称进行判断,比如IntBuffer只能操作Integer类型的对象。
实际上,使用最多的是ByteBuffer(二进制字节缓冲区)类型,后面的章节会看到它的具体使用。
2.1 Buffer类的重要属性
Buffer的子类会拥有一块内存,作为数据的读写缓冲区,但是读写缓冲区并没有定义在Buffer基类中,而是定义在具体的子类中。例如,ByteBuffer子类就拥有一个byte[]类型的数组成员
final byte[] hb;
可以作为自己的读写缓冲区,数据的元素类型与Buffer子类的操作类型相对应。为了记录读写的状态和位置,Buffer类额外提供了一些重要的属性,其中有三个重要的成员属性:capacity(容量)、position(读写位置)和limit(读写的限制)。
1、capacity属性
Buffer类的capacity属性表示内存容量的大小。一旦写入的对象数量超过了capacity,缓冲区就满了,不能在写入。capacity属性一旦初始化,就不能再改变。原因是什么呢?Buffer类的对象在初始化时会按照capacity分配内部数组的内存,在数据内存分配好之后,它的大小就不能改变了。前面讲到,Buffer类是一个抽象类,Java不能直接用来创建对象。在具体使用的时候,必须使用Buffer的某个子类,例如DoubleBuffer子类,该子类能写入的数据类型是double,如果在创建实例时,其capacity是100,那么最多可以写入100个double类型的数据。
【注意】capacity不是指内部的内存块byte[]数组的字节数量,而是指能写入的数据对象的最大限制数量。
2、position属性
position表示当前的位置。position属性的值与缓冲区的读写模式有关,在不同的模式下,position属性值的含义是不同的,在缓冲区进行读写的模式改变时,position值会进行相应的调整。
在写模式下,position值的变化规则如下:
(1)在刚进入写模式时,position值为0,表示当前的写入位置从头开始。
(2)每当一个数据写到缓冲区后,position就会向后移动到下一个可写的位置。
(3)初始的position值为0,最大可写值为limit-1。当position值达到limit时,缓冲区就已经无可用空间写入了。
在读模式下,position值的变化规则如下:
(1)当缓冲区刚开始进入读模式时,position会被重置为0.
(2)当从缓冲区读取时,也是从position位置开始读。读取数据后,position向前移动到下一个可读的位置。
(3)在读模式下,limit表示可读数据的上线。position的最大值为最大可读上线limit,当position达到limit时表示缓冲区已经无数据可读。
Buffer的读写模式具体如何切换呢?当新建了一个缓冲区实例时,缓冲区处于写模式,这时可以写入数据。在数据写入完成后,如果要从缓冲区读取数据,就要进行模式的切换,可以调用flip()方法将缓冲区变成读模式,flip为翻转的意思。再从写模式到读模式的翻转过程中,position和limit属性值会进行调整,具体的规则是:
(1)limit属性被设置成写模式时的position值,表示可以读取的最大数据位置。
(2)position由原来的写入位置变成新的可读位置,也就是0,表示可以从头开始读。
3、limit属性
limit属性表示可以写入或读取的数据最大上限,其属性值的具体含义也与缓冲区的读写模式有关。在不同的模式下,limit值的含义是不同的,具体分为以下两种情况。
(1)在写模式下,limit属性值的含义为可以写入的数据最大上限。在刚进入写模式时,limit的值会被设置成缓冲区的capacity值,表示可以一直将缓冲区的容量写满。
(2)在读模式下,limit值的含义为最大能从缓冲区读取多少数据。
一般来说,在进行缓冲区操作时是先写入再读取。当缓冲区写入完成后,就可以开始从Buffer读取数据,调用flip()方法,这时limit的值也会进行调整。具体如何调整呢?将写模式下的position值设置成读模式下的limit值,也就是说,将之前写入的最大数量作为可以读取数据的上限值。
Buffer在翻转时的属性值调整主要涉及position,limit两个属性,但这种调整比较微妙,不太好理解,下面举例说明:
首先,创建缓冲区。新创建的缓冲区处于写模式,其position值为0,limit值为最大容量capacity。
然后,向缓冲区写数据。每写入一个数据,position向后面移动一个位置,也就是position的值加1。这里假定写入了5个数,当写入完成后,position的值为5.
最后,使用flip()方法将缓冲区切换到读模式。limit的值会先被设置成写模式的position值,所以新的limit的值是5,表示可以读取数据的最大上限是5.之后调整position值,新的position会被重置为0,表示可以从0开始读。
缓冲区切换到读模式后就可以从缓冲区读取数据了,一直到缓冲区的数据读取完毕。
除了以上capacity、limit、position三个重要的成员属性外,Buffer还有一个比较重要的标记属性:mark(标记)属性。该属性的大致作用是:在缓冲区操作过程中,可以将当前的position值临时存入mark属性中,需要的时候,再从mark中取出暂存的标记值,恢复到position属性中,重新从position位置开始处理。
下面用表总结一下Buffer类的四个重要属性。