NIO源码系列之Buffer
- Buffer缓冲区
- 缓冲区存在的意义
-
- 基本属性
- 基本方法
-
- position
- limit
- mark
- reset
- clear
- flip
- remaining
- hasRemaining
- 子类
-
- IntBuffer
- HeapIntBuffer.allocate
- HeapIntBuffer.put
- HeapIntBuffer.get
- wrap(int[] array)
- wrap(int[] array, int offset, int length)
- HeapIntBuffer.get(int i)
- HeapIntBuffer.get(int[] dst, int offset, int length)
- HeapIntBuffer.put(int[] src, int offset, int length)
- put(IntBuffer src)
- HeapIntBuffer.put(IntBuffer src)
Buffer缓冲区
今天来研究下NIO三大核心之一的
Buffer
的基本原理。多说无益,我们看看源码,挑几个比较常用的说说,其他的自己看看就好了。
缓冲区存在的意义
为什么要有缓冲区呢,直接读写不就好了。其实这个涉及到硬件的问题,我就简单说几点:
- CPU的速度快,设备的速度慢,直接读写则CPU必定要有空闲等待设备的时间,造成CPU利用率不高,引入缓冲区后,设备可以以他较慢的速度向缓冲区写入数据,而CPU可以快速从缓冲区中读取,不必要等待设备。
- CPU去相应设备是需要中断的,设备发出中断请求,然后CPU把手头的事停了,过来取数据,然后再返回,中间涉及到上下文切换,保存现场,操作PCB块等,不展开讲了。如果可以有缓冲区,那就可以让设备把数据放缓冲区,满了之后,让CPU来取,而不是有点数据就让CPU来取,可以减少中断的频率,提高效率。就好像快递员不会每次就送一家快递,肯定是等某个区域的快递堆了差不多了,然后一次性送,就是这个道理。
- 提高CPU和设备之间并行度,双方不需要等待对方操作,一个可以只管读数据,一个只管放数据,互不应用。
- 缓冲区可以以数据块为单位存储,用DMA方式一次性读取大量数据,速度快,一次性获取大量数据到内存中,而不是传统的比如键盘输入的那种字符流,速度很慢。
说白就是提高CPU运行效率,提高数据传输的效率。
基本属性
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;//标记位置,用于重置回来,比如要做复读的功能,那就用这个标记好位置,然后重新从那个位置开始读
private int position = 0;//当前索引,如果与limit相等,表示已经不能操作了,操作完后会指向下一个位置
private int limit;//限制,限制的位置,这个位置是不可以操作的
private int capacity;//缓冲区的容量
long address;//跟直接缓冲区地址有关
前4个属性是主要操作的对象,最后一个和直接缓冲区相关后面会讲,我们可以画个图大致看下他们是怎么配合的:
其实
position
和
limit
就是两个指针,
position
指向当前位置,不管是读还是写,
limit
指向限制的位置,到了这里就不能操作了,
capacity
就是里面最多能操作的数据的个数。
mark
就是标记,可以将
position
指向标记位置,重新开始操作,比如实现从某个地方复读。
基本方法
position
其实就是设置新的操作位置:
public Buffer position(int newPosition) {
if (newPosition > limit | newPosition < 0)//如果新的位置超过了限制 或者<0 报异常
throw createPositionException(newPosition);
position = newPosition;//设置新的位置
if (mark > position) mark = -1;//如果标记大于新位置,标记为-1
return this;
}
limit
设置新的限制位置:
public Buffer limit(int newLimit) {
if (newLimit > capacity | newLimit < 0)//限制位置大于容量或者小于0, 报异常
throw createLimitException(newLimit);
limit = newLimit;
if (position > limit) position = limit;//限制位置大于当前位置,就把当前位置操作位置设定为限制位置
if (mark > limit) mark = -1;//标记位置大于限制为,标记设置为-1
return this;
}
mark
这里就是做标记啦,标记好这个位置,下次可以用:
public Buffer mark() {
mark = position;//标记成position
return this;
}
reset
重置到标记位置,可以实现复读这种功能:
public Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
clear
说是清空,其实是重置而已:
public Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
flip
操作翻转,也就是读写操作互换,比如我先写,写到
position
位置,那换成读的话当然就只能读到这里,后面没东西呀,所以
limit = position
,而且是从头开始读,所以
position = 0
,当然标记位已经无效了,操作都换了:
public Buffer flip() {
limit = position;//把限制设定为position
position = 0;//position从头开始
mark = -1;//清空标记
return this;
}
看看示意图:
remaining
剩余多少可操作,为什么不是返回
limit - position+1
呢,一般如果从
读到
5
,是
5-0+1=6
个可以操作的,但是这里
limit
这个位置是不能操作的,所以少了一个:
public final int remaining() {
return limit - position;
}
hasRemaining
是否还有剩余的数据可以操作:
public final boolean hasRemaining() {
return position < limit;
}
其他的一些暂时不说了,以后用到了会讲。
子类
我们可以看到他的直接子类,基本类型都涵盖了,除了
Boolean
,其实这些也是抽象类:
IntBuffer
我们拿
IntBuffer
来分析下,其他的子类都类似,先看看他的两个主要属性:
final int[] hb; //底层是个数组 // Non-null only for heap buffers
final int offset;//偏移量 真实位置是position+offset 比如我从第几位开始存offset设置为几
我们就拿
IntBuffer
来讲原理一下吧,其他的也差不多,我们先来看个简单的例子,申请一个10容量的缓冲区,然后放入8个数字,然后读出来:
public class NioTest {
public static void main(String[] args) {
IntBuffer intBuffer=IntBuffer.allocate(10);
for (int i = 0; i < 8; i++) {
intBuffer.put(i);
}
intBuffer.flip();
while (intBuffer.hasRemaining()){
System.out.println(intBuffer.get());
}
}
}
HeapIntBuffer.allocate
public static IntBuffer allocate(int capacity) {
if (capacity < 0)
throw createCapacityException(capacity);
return new HeapIntBuffer(capacity, capacity);
}
可以看到,内部是调用了子类
HeapIntBuffer
的构造函数:
HeapIntBuffer(int cap, int lim) { // package-private
super(-1, 0, lim, cap, new int[cap], 0);
this.address = ARRAY_BASE_OFFSET;
}
实则还是调用了:
IntBuffer(int mark, int pos, int lim, int cap, // package-private
int[] hb, int offset)
{
super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
}
就是创建一个容量的数组
new int[cap]
。
HeapIntBuffer.put
实际上也是调用
HeapIntBuffer
的
put
方法,将数据放到数据的相应位置,超过限制会报异常:
public IntBuffer put(int x) {
hb[ix(nextPutIndex())] = x;//将数组相应位置赋值
return this;
}
protected int ix(int i) {
return i + offset;//position位置+偏移量
}
final int nextPutIndex() { //如果没到限制,返回position,并将position右移一位
if (position >= limit)
throw new BufferOverflowException();
return position++;
}
看看示意图:
HeapIntBuffer.get
实际上也是调用
HeapIntBuffer
的
get
方法,获得相应位置的数据,超过限制会报异常:
public int get() {
return hb[ix(nextGetIndex())];
}
final int nextGetIndex() { // package-private
if (position >= limit)
throw new BufferUnderflowException();
return position++;
}
看看示意图吧:
wrap(int[] array)
一个包装,传入一个数组:
public static IntBuffer wrap(int[] array) {
return wrap(array, 0, array.length);
}
wrap(int[] array, int offset, int length)
上面的方法也是调用这个方法的,把
array
当做底层的数组,但是要注意
offset+length
不能超过数组长度,比如我
10
个容量的数组,
offset=5,length=10
这样其实就是
15
,超过
10
了,因为他表示是从数组索引
5
开始,然后放
10
个,也就是数组有
15
个了,
limit=15 > capacity=10
,会报异常。
public static IntBuffer wrap(int[] array,
int offset, int length)
{
try {
return new HeapIntBuffer(array, offset, length);
} catch (IllegalArgumentException x) {
throw new IndexOutOfBoundsException();
}
}
HeapIntBuffer(int[] buf, int off, int len) { // package-private
super(-1, off, off + len, buf.length, buf, 0);
this.address = ARRAY_BASE_OFFSET;
}
IntBuffer(int mark, int pos, int lim, int cap, // package-private
int[] hb, int offset)
{
super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
}
Buffer(int mark, int pos, int lim, int cap) { // package-private 包内私有
if (cap < 0)
throw createCapacityException(cap);
this.capacity = cap;
limit(lim);
position(pos);
if (mark >= 0) {
if (mark > pos)
throw new IllegalArgumentException("mark > position: ("
+ mark + " > " + pos + ")");
this.mark = mark;
}
}
HeapIntBuffer.get(int i)
这个方法就跟直接用索引取数组元素一样,不会去改变
position
。
public int get(int i) {
return hb[ix(checkIndex(i))];
}
@HotSpotIntrinsicCandidate //CPU指令高效实现
final int checkIndex(int i) { // package-private
if ((i < 0) || (i >= limit))//只判断有没越界
throw new IndexOutOfBoundsException();
return i;
}
HeapIntBuffer.get(int[] dst, int offset, int length)
表示从底层数组中读取
length
长度的数据放到
dst
数组中,从
dst
数组的索引
offset
处开始放置。注意这个是会改变
position
的。
public IntBuffer get(int[] dst, int offset, int length) {
checkBounds(offset, length, dst.length);//检查越界
if (length > remaining())//检查够不够读
throw new BufferUnderflowException();
System.arraycopy(hb, ix(position()), dst, offset, length);//拷贝
position(position() + length);//设置新位置,因为读取了length个数据,所以要改变position
return this;
}
HeapIntBuffer.put(int[] src, int offset, int length)
和get类似,就是把数组的
offset
索引开始的
length
个元素放入缓冲区:
public IntBuffer put(int[] src, int offset, int length) {
checkBounds(offset, length, src.length);
if (length > remaining())
throw new BufferOverflowException();
System.arraycopy(src, offset, hb, ix(position()), length);
position(position() + length);
return this;
}
put(IntBuffer src)
将整个
IntBuffer
放入,如果是本身或者只读,元素个数超过可以接受的就抛异常,否则就一个个取出来放进去:
public IntBuffer put(IntBuffer src) {
if (src == this)
throw createSameBufferException();
if (isReadOnly())
throw new ReadOnlyBufferException();
int n = src.remaining();
if (n > remaining())
throw new BufferOverflowException();
for (int i = 0; i < n; i++)
put(src.get());
return this;
}
HeapIntBuffer.put(IntBuffer src)
HeapIntBuffer
的覆盖:
public IntBuffer put(IntBuffer src) {
if (src instanceof HeapIntBuffer) {//堆缓冲区
if (src == this)
throw createSameBufferException();
HeapIntBuffer sb = (HeapIntBuffer)src;
int n = sb.remaining();
if (n > remaining())//如果要放进来的元素个数大于能接受的个数就抛异常
throw new BufferOverflowException();
System.arraycopy(sb.hb, sb.ix(sb.position()),//从sb.hb拷贝到hb
hb, ix(position()), n);
sb.position(sb.position() + n);
position(position() + n);
} else if (src.isDirect()) {//直接缓冲区
int n = src.remaining();
if (n > remaining())
throw new BufferOverflowException();
src.get(hb, ix(position()), n);//直接把src里的n个元素放到hb里,从postion+offset位置开始
position(position() + n);//更新位置
} else {
super.put(src);//一个个取出来放进去
}
return this;
}
另外还有直接缓冲区是用
Unsafe
类操作本地方法的,会涉及到属性
address
,具体有兴趣的可以看看c/c++源码,我暂时不分析了,以后有时间再看。
好了,今天就到这里了,希望对学习理解有帮助,大神看见勿喷,仅为自己的学习理解,能力有限,请多包涵。