上一篇介绍了Java中NIO和传统IO的区别及优势,这篇主要介绍NIO的核心组成。
  NIO共引入了4个概念:
- 缓存区:表示数据存放的容器,提供可读写的数据缓存区;
- 字符集:用来对缓存数据进行解码和编码,在字节和Unicode字符之间转换;
- 通道:用来接收或发送数据,提供与文件、套接字等的连接,类似于Java IO中的流;
- 选择器:他们与可选择通道一起定义了多路的、无阻塞的IO设施。
NIO框架位于Java.nio包中,它为每一个概念都提供了核心的支持类:
- 缓存区Buffer:包括ByteBuffer,Mapped
- ByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,ShortBuffer,IntBuffer,LongBuffer;
- 字符集Charset:java.nio.charset包中定义了字符集API,包括字符集Charset,编码器CharsetEncoder,解码器CharsetDecoder;
- 通道Channel:java.nio.channels包中定义了通道API,包括文件通道Filechannel,Socket通道SocketChannel,ServerSocket通道ServerSocketChannel,数据包通道DatagramChannel;
- 选择器Selector:包括选择器Selector和事件对象SelectionKey。

下面来了解着4个概念的含义和作用,并了解各个类包的接口和类的使用:

1. 缓存区Buffer:
   传统的IO不断浪费对象资源,NIO通过使用缓存区读写数据避免了资源浪费。缓存区是一个数据容器,就可以把它看作内存中的一个大的数组,用来存放来自Channel的同一类型的所有数据,因此我们可以使用字节、字符、整数等缓存区。字节缓存区提供必要的方法,可以提取或存入所有基本类型(boolean型除外)的数据。
  每个非布尔基本类型都有一个缓存区类,包括ByteBuffer,MappedByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,ShortBuffer,IntBuffer,Longbuffer。每个类定义了一系列用于将数据移除或移入缓存区中get()和put()方法,用于压缩、复制和切片缓存区的方法,以及用于分配新缓存区和将现有数组包装到缓存区中的静态方法。
  所有的缓存区类都有一个统一的抽象父类Buffer,它代表了一块内存区域,可以执行一些与内存有关的操作,如清除其中的内容,支持读写或只读等操作。该类提供了共有的4个属性和3个操作。下面详细看看这些属性和操作的含义和使用方法,然后详细了解8个实现类的使用。
①Buffer缓存区的4个基本属性:
Buffer是个抽象类,它的基本属性包括容量capacity、限制limit、位置position、标记mark。
a、capacity:这个Buffer最多能放多少数据,capacity一般在Buffer被创建的时候指定,取得容量的方法如下:

int capacity(); //返回此缓存区的容量

b、limit:在Buffer上进行的读写操作都不能越过这个下标。当写数据到Buffer中时,limit一般和capacity相等,当读数据时,limit代表Buffer中有效数据的长度,取得和修改限制的方法如下:

int limit();//返回此缓存区的限制
Buffer limit(int newLimit);//设置此缓存区的限制

c、position:读写操作的当前下标。当使用Buffer的相对位置进行读写操作时,读写会从这个下标进行,并在快操作完成后,Buffer会更新下标的值,取得和修改位置的方法如下:

int position(); //返回此缓存区的位置
BufferPosition(int newPosition);//设置此缓存区的位置

d、mark:一个临时存放的位置下标。调用mark()会将mark设为当前的position值,以后调用reset()会将position属性设置为mark的值。mark的值总是小雨等于position的值,如果将position的值设得比mark小,当前的mark值会被抛弃掉,可以使用下面的方法随时设置一个标记:

Buffer mark();//在此缓存区的位置设置标记

这些属性总是满足以下条件:
0<=mark<=position<=limit<=capacity
因此,标记、位置、限制和容量值遵守以下不变式:
0<=标记<=位置<=限制<=容量
对初始化的Buffer来说,position为0,limit=capacity。
②Buffer缓存区的3个数据操作:
a、清除clear():把position设为0,把limit设为capacity,一般在把数据写入Buffer前调用:

Buffer clear();//清除此缓存区

b、反转flip():把limit设为当前position,把position设为0,一般在从Buffer读取出数据前调用:

Buffer flip();//反转此缓存区

c、重绕rewind():把position设为0,limit不变,一般在把数据重新写入Buffer前调用

Buffer rewind();//重绕此缓存

Buffer对象有可能是只读的,这时任何对该对象的写操作都会触发一个ReadOnlyBufferException。isReadOnly()方法可以用来判断一个Buffer是否只读。
③Buffer缓存区的8个实现类:
对于每个非boolean基本类型,都有一个Buffer子类与之对应,这些子类位于包java.nio.Buffer中,他们的类关系图如下:
Buffer类关系图

2.字符集Charset——编码与解码
  向ByteBuffer中存放数据涉及到两个问题:字节的顺序和字符转换。ByteBuffer内部通过ByteOrder类处理了字节顺序问题,但是没有处理字符转换。事实上,ByteBuffer没有提供读写String。
  java.nio.charset.Charset处理了字符转换问题。它通过构造java.nio.charset.CharsetEncoder和java.nio.charset.CharsetDecoder将字符序列CharBuffer转换成字节ByteBuffer和逆转换。当需要将CharBuffer存放的数据编码成ByteBuffer时,再将ByteBuffer里存的数据解码成CharBuffer,这时候就要用到CharsetEncoder和CharsetDecoder了。
  CharsetDecoder用来将ByteBuffer解码为CharBuffer类型,CharsetEncoder用来将CharBuffer解码为ByteBuffer类型,其中的编码器和解码器都是通过Charset来创建的,它们之间关系如下:
字符转换过程
3.通道Channel
现有的java.io类中没有能够读写Buffer类型,所以NIO提供了Channel通道类来读写Buffer。Channel是一个连接,可用于接收或发送数据,如文件和套接字。因为channel连接的是底层的物理设备,它可以直接支持设备的读写,或提供文件锁。对于文件、管道、套接字都存在相应的Channel类。
  可以把Channel看成是数据流的替代品,它还通过Selector和SelectableChannel这两个类定义了一个进行非阻塞IO操作的API,这对需要高性能IO的应用非常重要。所有的Channel类都位于java.nio.channels包中,如下描述java.nio.channels中类和接口的关系:
Channel类关系图
图中最下面的4个类为具体的实现类。共包括7个接口类、3个抽象类和4个实现类。
接口类:
该图中包含了一系列的接口,用以分别实现不同的功能封装:
①Channel是最顶层的接口,代表一个可以进行IO操作的通道;
②ReadableByteChannel和WritableByteChannel分别提供对通道读取和写入ByteBuffer数据的功能;
③ByteChannel用来将读取和写入的功能合并;
④ScatteringByteChannel和GatheringByteChannel分别提供了批量读取和写入ByteBuffer数组的能力;
⑤InterruptibleChannel提供多线程异步关闭的能力,它覆盖了Channel接口中的关闭方法close(),这表现在两方面:
a、实现此接口的通道是可异步关闭的:如果某个线程阻塞于科终端通道上的IO操作,则另一个线程可调用该通道的close方法,这将导致已阻塞线程接收到AsynchronousCloseException,
b、实现此接口的通道也是可中断的:如果某个线程阻塞于科中断通道上的IO操作中,则另一个线程可调用该阻塞线程的interrupt方法。这将导致该通道被关闭,已阻塞线程接收到CloseByInterruptException,并且设置阻塞线程的中断状态。
  因此要拥有读写功能可以实现ByteChannel接口;要拥有批量读写功能,可以实现ScatteringByteChannel和GatheringByteChannel接口。从上图可看出,FileChannel、DatagramChannel、SocketChannel实现了这3个接口,因此它们都拥有批量读写的功能。
抽象类:
①AbstractInterruptibleChannel提供了可中断通道的基本实现,该类封装了实现通道异步关闭和中断所需的最低级别机制。在调用可能无限期阻塞的IO操作之前和之后,具体的通道类必须分别调用begin()和end()方法;
②SelectableChannel则提供了可通过Selector实现多路复用的通道:该抽象类是所有支持非阻塞IO操作的channel的父类。SelectableChannel可以通过register()方法注册到一个或多个Selector,以进行非阻塞IO操作,此方法返回一个表示该通道已向选择器注册的心SelectionKey对象。SelectableChannel还可以选择阻塞和非阻塞模式,这就是NIO非阻塞编程的核心。所有Channel创建的时候都是阻塞模式,只有非阻塞的SelectableChannel才可以参与非阻塞IO操作,可以使用下面方法来设置和查看阻塞模式:

SelectableChannel configureBlocking(boolean block); //设置blocking模式
boolean isBlocking();  //返回blocking模式

③AbstractSelectableChannel则提供了SelectableChannel中抽象函数的实现。此类定义了处理SelectableChannel通道注册、注销和关闭机制的各种方法。它会维持此通道的当前阻塞模式及其当前的选择键集。;
这三级抽象类都实现了InterruptibleChannel接口,所以也拥有多线程下异步关闭的能力。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐