天天看点

NIO源码系列之BufferBuffer缓冲区缓冲区存在的意义

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

的基本原理。多说无益,我们看看源码,挑几个比较常用的说说,其他的自己看看就好了。

缓冲区存在的意义

为什么要有缓冲区呢,直接读写不就好了。其实这个涉及到硬件的问题,我就简单说几点:

  1. CPU的速度快,设备的速度慢,直接读写则CPU必定要有空闲等待设备的时间,造成CPU利用率不高,引入缓冲区后,设备可以以他较慢的速度向缓冲区写入数据,而CPU可以快速从缓冲区中读取,不必要等待设备。
  2. CPU去相应设备是需要中断的,设备发出中断请求,然后CPU把手头的事停了,过来取数据,然后再返回,中间涉及到上下文切换,保存现场,操作PCB块等,不展开讲了。如果可以有缓冲区,那就可以让设备把数据放缓冲区,满了之后,让CPU来取,而不是有点数据就让CPU来取,可以减少中断的频率,提高效率。就好像快递员不会每次就送一家快递,肯定是等某个区域的快递堆了差不多了,然后一次性送,就是这个道理。
  3. 提高CPU和设备之间并行度,双方不需要等待对方操作,一个可以只管读数据,一个只管放数据,互不应用。
  4. 缓冲区可以以数据块为单位存储,用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个属性是主要操作的对象,最后一个和直接缓冲区相关后面会讲,我们可以画个图大致看下他们是怎么配合的:

NIO源码系列之BufferBuffer缓冲区缓冲区存在的意义

其实

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;
    }
           

看看示意图:

NIO源码系列之BufferBuffer缓冲区缓冲区存在的意义

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

,其实这些也是抽象类:

NIO源码系列之BufferBuffer缓冲区缓冲区存在的意义

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++;
    }
           

看看示意图:

NIO源码系列之BufferBuffer缓冲区缓冲区存在的意义

HeapIntBuffer.get

实际上也是调用

HeapIntBuffer

get

方法,获得相应位置的数据,超过限制会报异常:

public int get() {
        return hb[ix(nextGetIndex())];
    }
    final int nextGetIndex() {                          // package-private
	     if (position >= limit)
	         throw new BufferUnderflowException();
	     return position++;
    }
           

看看示意图吧:

NIO源码系列之BufferBuffer缓冲区缓冲区存在的意义

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++源码,我暂时不分析了,以后有时间再看。

好了,今天就到这里了,希望对学习理解有帮助,大神看见勿喷,仅为自己的学习理解,能力有限,请多包涵。

继续阅读