上一篇我们聊了高并发 IO 原理及模型,相信大家都基本掌握好了,So,我们今天继续来说说Java NIO,它是当今众多主流技术框架的基石,实属我们高性能通信旅程必备良方!
一、Java NIO简介
在JDK1.4之前,Java IO类库都是阻塞的IO;从1.4开始,引进了新的异步IO库,被称为Java New IO,简称 Java NIO,是一种非阻塞的IO,为Java提供了高速且带有缓冲区的IO,它属于 IO多路复用模型 ,主要包含以下三大组件:
- Channel 通道
- Buffer 缓冲区
- Selector 选择器
二、New IO与Old IO的PK
NIO(New IO)相较于OIO(Old IO),主要区别有以下几个:
- OIO是面向Stream的,NIO则是面向缓冲区。在一般的OIO中,我们以流的方式顺序读取一个或者多个字节,不能随意改变读取的位置;而在NIO中,通过Channel和Buffer,我们可以从通道读取数据到缓冲区中,或者将数据从缓冲区写入通道中,并且NIO可以随意读取缓冲区的任意数据;
- OIO是阻塞的,NIO是非阻塞的。比如我们调用read方法时,OIO模式下,整个线程会阻塞等待,直到read操作完成;而在NIO模式下,如果此时没有数据,则read直接返回,如果此时有数据,则read数据并返回。
- OIO没有Selector选择器,而NIO则是基于Selector实现,这个需要底层操作系统的支持。
下面进入NIO三大组件解说之旅,请大家坐好扶稳
三、Channel通道详解
在NIO中,同一个网络连接使用一个通道,所有的NIO的IO操作都是从通道开始的,它即可以从通道读取数据,也可以向通道写入数据。目前主要的Channel有如下四种:
- FileChannel:文件通道,用于文件数据的读写;
- Socket Channel:套接字通道,用于Socket套接字TCP连接的数据读写;
- ServerSocketChannel:服务器套接字通道,允许我们监听TCP连接的请求,为每个监听到的请求创建一个相应的Socket套接字通道;
- DatagramChannel:数据报通道,用于UDP协议的数据读写。
现在说一下FileChannel的用法,加深大伙对NIO的一个印象,这里通过RandomAccessFile进行文件的读取操作:

四、Buffer缓冲区详解
应用程序与通道的主要交互,就是进行数据的读取与写入,在他们之间有一个缓冲区,缓冲区本质上是一个内存块,提供了一组有效的方法来进行写入和读取的交替访问。
Buffer类是一个抽象类,在NIO中有8种缓冲区类,分别如下:
- 整型Buffer:ByteBuffer、IntBuffer、LongBuffer
- 浮点型Buffer:FloatBuffer、DoubleBuffer
- 字符型Buffer:CharBuffer
- 内存映射型Buffer:MappedByteBuffer
Buffer类的重要属性
Buffer类底层是由一个byte[]数组构成,为了记录读写的状态和位置,它提供了一些重要的属性,其中,比较重要的有如下三个:capacity(容量)、 position (读写位置)、limit(读写限制)。
除此之外,还有一个标记属性mark,可以临时保存 当前position,配合reset属性,可以恢复到之前mark标记的position位置。

1、capacity属性
capacity表示内部容量的大小,一旦写入的数据超过了capacity容量大小,将不能再写入。同时,在它初始化后,也不能再改变了。
2、position属性
这里需要注意一下,position属性与缓冲区的读写模式有关,在不同的模式下,position属性的值是不一样的,当读写模式互相切换的时候,position会进行相应的调整,规则如下
- 在写入模式下,position的值变化规则如下:(1)在刚进入到写模式时,position值为0,表示当前的写入位置为从头开始。(2)每当一个数据写到缓冲区之后,position会向后移动到下一个可写的位置。(3)初始的position值为0,最大可写值position为limit-1。当position值达到limit时,缓冲区就已经无空间可写了。
- 在读模式下,position的值变化规则如下:(1)当缓冲区刚开始进入到读模式时,position会被重置为0。(2)当从缓冲区读取时,也是从position位置开始读。读取数据后,position向前移动到下一个可读的位置。(3)position最大的值为最大可读上限limit,当position达到limit时,表明缓冲区已经无数据可读。
缓冲区创建成功之后,默认处于写模式,数据写入后,如果要读取数据,这时要进行模式切换,我们可以使用flip方法,将写模式切换为读模式,在这个flip翻转过程中,position会进行非常巨大的调整,具体的规则是:position由原来的写入位置,变成新的可读位置,也就是0,表示可以从头开始读。flip翻转的另外一半工作,就是要调整limit属性。
3、limit属性
limit表示读写的最大上限,它也和缓冲区的读写模式有关,在不同的模式下,它的含义也是不一样的。
- 在写模式下,limit属性值的含义为可以写入的数据最大上限。在刚进入到写模式时,limit的值会被设置成缓冲区的capacity容量值,表示可以一直将缓冲区的容量写满。
- 在读模式下,limit的值含义为最多能从缓冲区中读取到多少数据。
如何使用Java NIO Buffer?下面举个简单的栗子
- 使用Buffer子类创建实例对象,并且分配内存空间,这里调用子类的 allocate ()方法即可;
- 调用put方法,将数据写入至缓冲区;
- 写入成功后,调用flip()方法,从开始的写模式切换为读取模式;
- 调用get方法,从缓冲区读取数据;
- 读取完成后,调用clear()或者compact()方法,将缓冲区转换为写入模式,此方法会将position清0,limit设置为capacity的最大容量值。
五、Selector选择器详解
Selector是实现IO多路复用的关键,它属于一个IO事件查询器,通过这个查询器,我们可以只用一个线程,即可获取多个通道的IO事件状态,这是非常高效的一个行为。实现IO多路复用,从开发层面来说,首先把通道注册到选择器中,然后通过选择器来查询这些注册的通道是否有已经就绪的IO事件,比如可读、可写、网络连接完成等。相比OIO的每个网络连接对应一个线程,NIO大大减小了系统开销。
Selector选择器和通道的关系密切,它们通过注册的方式连接在一起。调用通道的Channel.register(Selector sel, int ops)方法,可以将通道注册到一个选择器中,这个方法的第一个参数代表目标通道需要注册到的选择器,第二个参数代表选择器需要监控的IO事件类型。
可供监听的IO事件类型有如下四种:
- SelectionKey.OP_READ:可读
- SelectionKey.OP_WRITE:可写
- SelectionKey.OP_CONNECT:连接
- SelectionKey.OP_ACCEPT:接收
如何使用选择器?
- 获取选择器实例;
- 将通道注册到选择器中;
- 轮询感兴趣的IO就绪事件。
以下为服务端使用选择器的一个小栗子,供大家参考
public class ServerSocket {
private Selector selector = null;
private ByteBuffer readByteBuffer = ByteBuffer.allocate(1024);
public static void main(String[] args) {
ServerSocket serverSocket = new ServerSocket(9988);
serverSocket.serverRun();
}
public ServerSocket(int port) {
try {
this.selector = Selector. open ();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();//创建服务端通道
serverSocketChannel.configureBlocking(false);//服务端通道设置为非阻塞
serverSocketChannel.bind(new InetSocketAddress(port));//绑定服务端端口
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//将服务端通道注册到选择器,并且监听连接事件
System.out.println(“服务端初始化成功,端口为” + port);
} catch (Exception e) {
e.printStackTrace();
}
}
public void serverRun() {
//循环监听
while (true) {
try {
//多路复用开始监听
int num = this.selector.select();
if (num == 0) {
continue;
}
//返回可选择的key集合
Set<SelectionKey> selectionKeys = this.selector.selectedKeys();
//遍历结果集进行处理
Iterator<SelectionKey> it = selectionKeys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
if (key.isValid()) {
if (key.isAcceptable()) {
//通道是否有新连接
System.out.println(“acceptable..”);
}
if (key.isConnectable()) {
//通道连接成功
System.out.println(“connectable..”);
}
if (key.isReadable()) {
//通道可读
System.out.println(“readable..”);
}
if (key.isWritable()) {
//通道可写
System.out.println(“writable..”);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}