Java NIO可以理解为,非阻塞IO(Non-blocking I/O),或者称之为,新的IO(New I/O)。比较核心的组件是:
(1)Buffer,缓冲区,用于存储数据。
(2)Channel,通道,数据传输的载体。
(3)Selector,选择器或者称为多路复用器,用于监听Channel,实现一个线程监听多个通道,减少线程上下文切换的开销。
本篇博客主要介绍的是Buffer,除了boolea类型之外,其他基本数据类型都有对应的Buffer,都继承于java.nio.Buffer,如下:
下面以ByteBuffer为例,介绍Buffer的一些原理和用法。
首先,明确一点,Buffer实质就是数组,别把它看得过于神秘,相关操作也是对数组的操作而已。
先看抽象类Buffer中定义的四个属性:
// 四者之间的大小关系: mark <= position <= limit <= capacity
private int mark = -1; // 用于标记position的位置
private int position = 0; // 下一个要读或要写的数组下标
private int limit; // 能够读或者写的个数
private int capacity; // 数组的容量,不会改变
源码中,也给出了四者之间大小的关系。由于capacity一旦确定,就不会改变,所以不是我们关注的重点。缓冲区有读模式和写模式之分:
在写模式下:
(1)position代表下一个可以写入的数据的数组下标
(2)limit,是限制的意思,代表最多可以写入多少个数据,大小等于capacity
在读模式下:
(1)position代表下一个可以读取数据的数组下标
(2)limit,代表最多可以读取多少个数据,等于上一次写模式下的position
下面通过一些简单的例子进一步理解它们:
public static void main(String[] args) {
String data = "abc123";
// 分配指定大小的空间
ByteBuffer buffer = ByteBuffer.allocate(1024);
printMsg("before put data",buffer);
// 把数据放入缓存中
buffer.put(data.getBytes());
printMsg("after put data",buffer);
// 从写模式,切换为读模式
buffer.flip();
printMsg("after flip()",buffer);
byte[] dest = new byte[buffer.limit()];
// 从缓存中取数据
buffer.get(dest, 0, 3);
printMsg("第一次取出的数据:"+new String(dest,0,3),buffer);
// 标记position的位置
buffer.mark();
// 从缓存中取数据
buffer.get(dest, 0, 2);
printMsg("第二次取出的数据:"+new String(dest,0,2),buffer);
// 重置
buffer.reset();
printMsg("after reset data",buffer);
}
// 打印信息
public static void printMsg(String title,ByteBuffer buffer){
System.out.println(title);
System.out.println("position: "+buffer.position());
System.out.println("limit: "+buffer.limit());
System.out.println("========================");
}
1.没有放数据之前:
// 分配指定大小的空间
ByteBuffer buffer = ByteBuffer.allocate(1024);
printMsg("before put data",buffer);
我们申请了1024个字节的缓冲区,其初始值为:
before put data
position: 0
limit: 1024
2.写入数据到缓冲区之后
// 把数据放入缓存中
buffer.put(data.getBytes());
printMsg("after put data",buffer);
我们往缓冲区放了 abc123 ,由于英文和数字占一个字节,故一共占6个字节,将数组从下标0-5填充了数据,故postion为6,即下一个可写入的下标:
after put data
position: 6
limit: 1024
查看其源码:
public final ByteBuffer put(byte[] src) {
return put(src, 0, src.length);
}
调用了重载方法:
public ByteBuffer put(byte[] src, int offset, int length) {
checkBounds(offset, length, src.length);
if (length > remaining())
throw new BufferOverflowException();
int end = offset + length;
for (int i = offset; i < end; i++)
this.put(src[i]);
return this;
}
最后还是调用了存单个字节的方法:
this.put(src[i]);
3.从写模式,切换为读模式:
// 从写模式,切换为读模式
buffer.flip();
printMsg("after flip()",buffer);
查看其输出:
after flip()
position: 0
limit: 6
可以看到,其postion已经重置为0了,表示将从0开始读取数据,而limit的值,就是上次写模式下的position,看其源码就明白了:
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
4.第一次读取数据并mark():
byte[] dest = new byte[buffer.limit()];
// 从缓存中取数据
buffer.get(dest, 0, 3);
printMsg("第一次取出的数据:"+new String(dest,0,3),buffer);
// 标记position的位置
buffer.mark();
此次读取了3个字节的数据,position应为3,limit还是6:
第一次取出的数据:abc
position: 3
limit: 6
看看其get方法都做了什么:
public ByteBuffer get(byte[] dst, int offset, int length) {
checkBounds(offset, length, dst.length);
if (length > remaining())
throw new BufferUnderflowException();
int end = offset + length;
for (int i = offset; i < end; i++)
dst[i] = get();
return this;
}
最终还是读取了获取单个字节的方法:
dst[i] = get();
5.第二次读取
buffer.get(dest, 0, 2);
printMsg("第二次取出的数据:"+new String(dest,0,2),buffer);
这次只读了2个字节的数据,所以position应为5,limit还是6:
第二次取出的数据:12
position: 5
limit: 6
这次操作,主要是为了演示说明mark()方法和reset()方法的作用,加深理解
6.reset重置到mark的位置
buffer.reset();
printMsg("after reset data",buffer);
通过调用reset方法,可以将position恢复到上一次mark的位置,因为mark记录了position的值:
after reset data
position: 3
limit: 6
所以position的值恢复到了,第一次读取数据之后的值,也就是3,这样就可以实现对某部分数据的重复读取了。
其源码如下:
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
关键的一点是,将mark的值重新赋值给position:
position = m;
其实,把缓冲区当做数组就很好理解了