Java的NIO,即New I/O,也叫Non-Blocking I/O,作为一个Java 1.4就推出的特性,其实老早就听说了。平时项目中用到的各种Web容器啥的,比方说Tomcat、Jetty,内部都用到了NIO,更别提像Netty这种专门的NIO框架了。最近抽了点时间,对它做了一点简单的了解。

概述

首先,NIO包含3个核心的组件,分别是java.nio.Buffer、java.nio.channels.Channel和java.nio.channels.Selector,即通道、缓冲和选择器。简单地说,数据从通道读入缓冲,或从缓冲写入通道(Channels read data into Buffers, and Buffers write data into Channels),而选择器用于检测通道的事件。

相比于面向字节或者字符的BIO,在NIO中,一个线程在(发起)从读数据到缓冲区以后,可以去做其他的事,直到读取完成,再回来继续处理,写也是类似的,因而说NIO是非阻塞的。

常见的Channel包括

  • java.nio.channels.FileChannel
  • java.nio.channels.SocketChannel
  • java.nio.channels.ServerSocketChannel
  • java.nio.channels.DatagramChannel

从名字上应该不难猜出,它们分别是用于处理文件、TCP和UDP的通道。

常见的Buffer包括

  • java.nio.ByteBuffer
  • java.nio.CharBuffer
  • java.nio.DoubleBuffer
  • java.nio.FloatBuffer
  • java.nio.IntBuffer
  • java.nio.LongBuffer
  • java.nio.ShortBuffer

这个看起来比通道更清爽,除去Boolean,其余7种基本数据类型分别有各自对应的缓冲啊哈~此外,ByteBuffer有一个子类MappedByteBuffer,根据API说明,它用于内存到文件的直接映射。

Buffer

Buffer的本质上就是一块内存区域,示意图如下。图中有3个索引,position、limit和capacity,分别指代下一个要操作的元素的位置、下一个不可操作的元素的位置和Buffer的总长度。此外,我们把limit-position的值记为remaining,意为Buffer中当前剩余元素的个数。


(图源:《深入分析JavaWeb技术内幕(修订版)》,许令波)

首先来创建一个Buffer,以ByteBuffer为例,此时position=0,limit=16,capacity=16,表示缓冲区容量16,接下来操作0号位置,最多可以操作到16号位置(不含)

1
ByteBuffer buffer = ByteBuffer.allocate(16);

接着往缓冲区里放入三个元素(大写字母A、B、C),看到position=3,limit和capacity无变化,表明当前缓冲区前3个位置当前已经有数据了。

此外java.nio.ByteBuffer#put(byte)方法还有几个变体,例如java.nio.ByteBuffer#putInt(int),用于向缓冲区里放其他类型的数据,但如果往缓冲区里放超过其容量个元素,将抛出java.nio.BufferOverflowException。

1
2
3
for (int i = 'A'; i < 'D'; i++) {
buffer.put((byte) i);
}

既然可以用put来放数据,必然就有get来取数据,方法同样有多个,包括java.nio.ByteBuffer#get()、java.nio.ByteBuffer#getInt()等。

且慢!get取出数据的同时,position仍然在往前移动啊!前面我们向Buffer添加了3个元素,如果直接get3次,非但取出的是3个0(position为3、4、5的元素),还把position移到了6号位置。

所以我们要先调用一下java.nio.Buffer#flip()方法,它的作用是另limit=position,然后使position=0,这样我们既可以重新访问刚才放入Buffer的数据、又不会访问到未使用的位置了(position不能超过limit),因此有些资料中说这个方法用于“Buffer的读/写”。

1
2
3
4
5
buffer.flip();
//从缓冲区取数据
for (int i = 0; i < 3; i++) {
System.out.println(buffer.get());
}

如果需要重新读已经读过的Buffer,可以使用java.nio.Buffer#rewind()方法,它只将position复位,不改变limit

1
2
3
4
5
buffer.rewind();
//又可以愉快地重新读啦
for (int i = 0; i < 3; i++) {
System.out.println(buffer.get());
}

读也读够了,接下来呢?重新把Buffer用在别的地方?那就需要先清除里面旧的东西,使用java.nio.Buffer#clear()方法,它令position=0,limit=capacity,也就是Buffer刚创建出来的样子。注意哦,里边的数据并没有真正被清掉,这和我们删除磁盘文件的道理有点像。

换一种特殊一点的情况,Buffer我们暂时只读了一部分(比方说写入了10字节,先读出5字节),这时我希望把已经读过的那5个字节的空间让出来。那就需要用java.nio.ByteBuffer#compact()方法“压缩”一下,其原理是1.使用java.lang.System#arraycopy()把Buffer中未读取的数据(position到limit-1部分)复制到Buffer的开头;2.重新设置position=limit-position(即remaining)、limit=capacity。

1
2
3
4
//压缩缓冲区
buffer.compact();
//清除缓冲区
buffer.clear();

此外补充一个前面略过的索引——mark,它的初始值是-1,可以调用java.nio.Buffer#mark()方法在当前position处“打个标签”,然后在需要的时候调用java.nio.Buffer#reset(),把position设置到mark过的地方。注意,flip、rewind、clear、compact都会导致mark被自动清除。

这一篇主要就讲讲什么是NIO,以及Buffer相关的东西,其他内容下一篇见~