说到IO大家并不陌生,平时的日志存储、网络请求都涉及到IO。这些IO的底层工具基本上是HTTP Client或者Netty,而这两个工具的底层则都是Java的NIO包。
Java中的NIO是New I/O的意思,它解决了BIO中(如InputStream和OutputStream)存在种种缺点,例如BIO是阻塞的、效率低等问题。
Java中的BIO是Blocking I/O的意思,意为同步阻塞的IO。
同步、异步、阻塞、非阻塞可以组合成以下4种排列:
同步阻塞
同步非阻塞
异步阻塞
异步非阻塞
在使用BIO(如InputStream何OutputStream)时,就是属于同步阻塞,因为执行当前读写任务一直是当前线程,并且读不到或写不出去就一直是阻塞状态。阻塞的意思就是方法不返回,直到读到数据或写出数据为止。
NIO技术属于同步非阻塞,也是由当前线程在执行读写操作,但是读不到数据或数据写不出时方法就返回了,继续执行读或写后面的代码。
异步阻塞是指A、B两线程间通信。例如A线程发起读操作,让B线程进行实现,A、B线程就是异步执行了。这时B线程进行读取操作,如果读不到则呈阻塞状态。如果读到了就通知A线程,并且将数据交给A线程。
异步非阻塞,是指A线程发起读操作,让B线程进行实现,因为A线程还要继续做其他的事情,这时B线程开始工作,如果读不到数据,B线程就继续执行后面的代码,直到读到数据时,B线程就通知A线程,并且将拿到的数据交给A线程。
本篇文章分为四个章节,将着重介绍JavaNIO的buffer、channel、selector的使用方法和部分源码实现。
Buffer是一个用于存储基本数据类型值的容器,类似数组来组织数据。
Buffer类是一个抽象类,它具有7个直接子类,每个基本类型(除去boolean)都有一个子类与之对应。
子类分别为:IntBuffer、CharBuffer、ShortBuffer、LongBuffer、FloatBuffer、DoubleBuffer、ByteBuffer。
Buffer及其子类提供很多了工具方法,方便进行数据处理。但是也是由于方法众多,造成了业界对NIO的诟病,即NIO晦涩难懂,不好用。
这是Buffer父类及子类ByteBuffer的方法列表:
Buffer的方法主要操作四个值,分别为:
capacity(容量) 代表包含元素的数量
limit(限制) 代表第一个不能读取或写入的元素的索引
position(位置) 代表下一个要读取或写入的元素的索引
mark(标记) 在此缓冲区的位置设置标记
四个值的大小关系是:0 ≤ mark ≤ position ≤ limit ≤ capacity。如图所示:
下面就Buffer类的常用方法做源码解读。
1、public final int capacity()
返回此缓冲区的容量。
2、public final int limit()
返回此缓冲区的限制。
3、public final Buffer limit(int newLimit)
设置此缓冲区的限制。如果设置的限制大于容量,或者限制小于0时程序报错。如果设置的限制小于位置,将位置设置为限制。如果设置的限制小于标记,将标记删除。
4、public final int position()
返回此缓冲区的位置。
5、public final Buffer position(int newPosition)
设置此缓冲区的位置。如果设置的位置大于限制或者设置的位置小于0,程序报错。如果设置的位置小于标记,删除标记。
6、public final Buffer mark()
在此缓冲区的位置上设置一个标记。将位置的值赋给标记。
7、public final Buffer clear()
还原缓冲区到初始状态,将位置设为0,将限制设置为容量,删除标记。
下面将以IntBuffer类来讲解其他方法。
8、public static IntBuffer wrap(int[] array, int offset, int length)
将数组放入缓冲区中,构建存储类型为int的缓冲区。数组长度值赋给capacity,offset的值赋给position,offset+length的值赋给limit。
9、public IntBuffer put(int x)
将给定数据放入缓冲区的position位置上,并将position加1。
10、public int get()
读取当前position位置的数据,并将position加1。如果当前position大于limit,抛出异常。
11、public final Buffer rewind()
重绕缓冲区。将位置设置为0并删除标记。通常在重新读取缓冲区数据时使用。
12、public static ByteBuffer allocateDirect(int capacity)
创建直接缓冲区。直接缓冲区使用的是堆外内存,在内核中进行IO操作,避免了数据复制,也就是常说的NIO的"零复制"
在NIO中,要将操作的数据打包到缓冲区中,而缓冲区中的数据想要传输到目的地是要依赖于通道的。缓冲区是将数据打包,通道是将数据进行传输。
Channel是一个接口,Channel接口有很多子接口,这些子接口又有很多实现类。在Java1.8中,Channel一共有11个子接口。
这些接口的作用分别是:
AsynchronousChannel接口的主要作用是使通道支持异步I/O操作。异步通道在多线程并发情况下是线程安全的。某些通道的实现是可以支持并发读和写的,但是不允许在一个未完成的I/O操作上再次调用read或write操作。
AsynchronousByteChannel接口的主要作用是使通道支持异步I/O操作,操作单位为字节。
ReadableByteChannel接口的主要作用是使通道允许对字节进行读操作。接口只允许有1个读操作在进行。如果1个线程正在1个通道上执行1个read()操作,那么任何试图发起另一个read()操作的线程都会被阻塞,直到第1个read()操作完成。
ScatteringByteChannel接口主要作用是可以从通道中读取字节到多个缓冲区中。
WritableByteChannel接口的主要作用是使通道允许对字节进行写操作。接口只允许有1个写操作在进行。如果1个线程正在1个通道上执行1个write()操作,那么任何试图发起另一个write()操作的线程都会被阻塞,直到第1个write()操作完成。
GatheringByteChannel接口主要作用是可以将多个缓冲区中的数据写入到通道中。
ByteChannel接口的主要作用是将ReadableByteChannel与WriteableByteChannel的规范进行了统一。ByteChannel接口的实现类就具有了读和写方法,是双向的操作。而单独的实现ReadableByteChannel与WriteableByteChannel接口就是单向的操作,只能进行读操作或只能进行写操作。
SeekableByteChannel接口主要作用是在字节通道中维护position位置信息,以及允许position发生该变。
NetworkChannel接口主要作用是使通道与Socket进行关联,使通道中的数据能在Socket技术上进行传输。
MulticastChannel接口主要是是通道支持IP多播。同时向IP组内多个主机传输数据。
InterruptibleChannel接口主要作用是使通道能以异步的方式进行关闭与中断。
FileChannel类的主要作用是读取、写入、映射和操作文件的通道。该通道永远是阻塞的。FileChannel类在内部维护当前文件的position,可对其进行查询和修改。下面就FileChannel类的方法做解读。
1、public abstract int write(ByteBuffer src) throws IOException;
将remaining字节序列从给定的缓冲区写入此通道的当前位置。在任意给定时刻,一个可写入通道上只能进行一个写入操作。如果在第一个写入操作完成之前发起其他写入操作,那么在第一个操作完成之前将阻塞其他写入操作。
2、public abstract int read(ByteBuffer dst) throws IOException;
将字节序列从此通道的当前位置读入给定的缓冲区的当前位置。在任意给定时刻,一个读取通道上只能进行一个读取操作。如果在第一个读取操作完成之前发起其他读取操作,那么在第一个操作完成之前将阻塞其他读取操作。
3、public abstract long write(ByteBuffer[] srcs, int offset, int length) throws IOException;
以指定缓冲区数组的offset下标开始,向后使用length个字节缓冲区,再将每个缓冲区的remaining剩余字节子序列写入此通道的当前位置。
4、 public abstract long read(ByteBuffer[] dsts, int offset, int length) throws IOException;
将通道中当前位置的字节序列读入以下标为offset开始的ByteBuffer[]数组中的remaining剩余空间中,并且连续写入length个ByteBuffer缓冲区。
5、public abstract int write(ByteBuffer src, long position) throws IOException;
将缓冲区的remaining剩余字节序列写入通道的指定位置。
6、public abstract int read(ByteBuffer dst, long position) throws IOException;
将通道的指定位置的字节序列读入给定的缓冲区的当前位置。
在使用选择器时,主要由Selector、SelectionKey和SelectableChannel三个对象来实现线程选择某个通道进行业务处理。
Selector选择器是NIO技术中的核心组件,可以将通道注册进选择器中,其主要作用就是使用1个线程来对多个通道中的已就绪通道进行选择,然后就可以对选择的通道进行数据处理。就是使用1个线程来操作多个通道,这种机制在NIO技术中称为"I/O多路复用"
Selector类的主要作用是作为SelectableChannel对象的多路复用器。可通过调用Selector类的open()方法创建选择器,该方法将使用系统的默认SelectorProvider创建新的选择器。在通过选择器的close()方法关闭选择器之前,选择器一直保持打开状态。
SelectionKey类标识SelectableChannel在选择器中的注册的标记。在每次向选择器注册通道时,就会创建一个选择键(SelectionKey)。
通过调用Selectorl类的selecterdKeys()方法,获取已就绪的键集。循环键集,获取键所指向的就绪的通道,即可操作通道中的数据。
NIO技术使用I/O多路复用技术,少量线程即可操作多个通道,使用同步非阻塞的方式操作IO,减少了CPU和内存的消耗。
https://my.oschina.net/u/3824443/blog/2875430
《NIO与Socket编程技术指南》