NIO 基础组件之 Buffer

14,096 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第17天,点击查看活动详情

生活的道路一旦选定,就要勇敢地走到底,决不回头 —— 左拉

NIO 基础组件之 Buffer

什么是Buffer缓冲区

定义

    作为数据的读写缓冲区,但是读写缓冲区并没有定义在Buffer基类中,定义在具体的子类中了,比如 IntBuffer、DoubleBuffer、CharBuffer、FloatBuffer、ByteBuffer、LongBuffer、ShortBuffer

基本属性

1. capacity

    缓冲区的容量,也就是元素的个数,永远不会改变,一旦写入的元素数量超过了定义的capacity的数量,那么缓冲区就满了,并且定义好大小就不能改变了,应为这块内存已经分配好了。


2. limit

    缓冲区的限制,不应读取或者写入的第一个元素的索引,limit <= capacity,表示可以写入或者读取的最大数据上限,他和读写模式有关系

  • 写模式

    当前模式下就是可写入数据的最大上限,刚开始是limit == capacity的,当前缓冲区为空,可以写满

  • 读模式

    读模式下就是目前可以读取多少数据,也就是缓冲区中有的多少数据

  • 读写模式怎么转换
    • 就是将写模式下的position【此时position==limit-1 】的值设置成读模式下的 limit【此时limit ==0】的值,也就是写模式下的位置【position】索引设置成了读模式下的限制[limit]索引

3. position

    缓冲区的位置, 是要读取或者写入的下一个元素的索引,position <= limit,该值与缓冲区的的读、写模式有关

  • 写模式
    1. 刚进入写模式的时候此时 position 从0开始
    2. 每写一次,position 的索引 +1
    3. 初始的position的值为0,最大的可写值为limit -1 ,此时limit 是等于 capacity的,当position = limit -1 的时候表示缓冲区已经无空间可以写了
  • 读模式
    1. 刚进入读模式的时候,position 会被重置为0
    2. 当从缓冲区读取数据的时候也是从position位置开始读取的,读取数据后,position的索引 +1
    3. 在读模式下,limit表示可读数据的上限,position的值最大等于limit,当position = limit的时候,表明缓冲区已经没有数据可以读取了
  • 读写模式怎么转换
    1. 新建的缓冲区是处于写模式的,当写完数据后想要读的话需要进行flip方法调用,也就是将缓冲区变成读模式
    2. 但是转换的过程中需要将position 和 limit 进行转变,当然了capacity是不变化的
    3. limit 被设置成写模式的时候的position的值,表示可读取的最大数据位置
    4. position 由原来的写入位置变成新的可读取的位置,也就是0

举例说明读写模式转换的参数变化

  1. 首先,创建缓冲区。新创建的缓冲区处于写模式,其position值为0,limit值为最大容量capacity。
  2. 然后,向缓冲区写数据。每写入一个数据,position向后面移动一个位置,也就是position的值加1。这里假定写入了5个数,当写入完成后,position的值为5。
  3. 最后,使用flip方法将缓冲区切换到读模式。limit的值会先被设置成写模式时的position值,所以新的limit值是5,表示可以读取数据的最大上限是5。之后调整position值,新的position会被重置为0,表示可以从0开始读。缓冲区切换到读模式后就可以从缓冲区读取数据了,一直到缓冲区的数据读取完毕。

特殊参数

1. mark

    缓冲区的标记是先调用mark()方法设置mark = position,之后再调用reset()方法时将其position的位置重置到mark处【position = mark】,并且mark 标记的索引位置永远小于等于 position的索引位置


以上四个参数满足于下面的规则

具体参数大小限制逻辑

    0 <= mark <= position <= limit <= capacity


常用方法

1. allocate()

    分配内存空间,其实我们再用Buffer的实例的时候,首先获取Buffer子类的实例对象,并且分配内存空间,这个分配内存空间的操作就是 allocate() 方法

public class NioDemo {
    public static void main(String[] args) {
        IntBuffer buffer = IntBuffer.allocate(10);
        System.out.println("capacity : " + buffer.capacity());
        System.out.println("limit : " + buffer.limit());
        System.out.println("position : " + buffer.position());
    }
}

image.png
    目前这个buffer 处于写模式,position 处于索引 0 位置,capacity 是申请的buffer 缓冲区的大小就是10,limit 目前和capacity 一样
源码分析

# IntBuffer类
// 创建一个int类型的缓冲区
public static IntBuffer allocate(int capacity) {
    if (capacity < 0)
        throw new IllegalArgumentException();
    return new HeapIntBuffer(capacity, capacity);
}
# HeapIntBuffer类
HeapIntBuffer(int cap, int lim) {         
    super(-1, 0, lim, cap, new int[cap], 0);
}
# IntBuffer类
IntBuffer(int mark, int pos, int lim, int cap, int[] hb, int offset){
    super(mark, pos, lim, cap);
    this.hb = hb;
    this.offset = offset;
}
# Buffer类
Buffer(int mark, int pos, int lim, int cap) { 
    if (cap < 0)
        throw new IllegalArgumentException("Negative capacity: " + cap);
    this.capacity = cap;
    limit(lim);
    position(pos);
    if (mark >= 0) {
        if (mark > pos)
            throw new IllegalArgumentException("mark > position: ("
                                               + mark + " > " + pos + ")");
        this.mark = mark;
    }
}

limit(lim)方法

# Buffer类
public final Buffer limit(int newLimit) {
    if ((newLimit > capacity) || (newLimit < 0))
        throw new IllegalArgumentException();
    limit = newLimit;
    if (position > limit) position = limit;
    if (mark > limit) mark = -1;
    return this;
}

position方法

# Buffer类
public final Buffer position(int newPosition) {
    if ((newPosition > limit) || (newPosition < 0))
        throw new IllegalArgumentException();
    position = newPosition;
    if (mark > position) mark = -1;
    return this;
}

2. put()

    往缓冲区中写入元素

public class NioDemo {
    public static void main(String[] args) {
        IntBuffer buffer = IntBuffer.allocate(10);
        for (int i = 0; i < 5; i++) {
            buffer.put(i);
        }
        System.out.println("capacity : " + buffer.capacity());
        System.out.println("limit : " + buffer.limit());
        System.out.println("position : " + buffer.position());
    }
}

image.png
    往缓冲区中写入了5个元素,之后缓冲区的三个属性值变化是,capacity 是永久不变的,limit 目前没有变化,不过position 已经从 0 变成了 5,这个是5 为什么不是4 ,是因为这个5不是代表他写到了哪里,是他下一个要写的元素的索引位置。
源码分析

# HeapIntBuffer类
final int[] hb; 

public IntBuffer put(int x) {
    hb[ix(nextPutIndex())] = x;
    return this;
}

protected int ix(int i) {
    return i + offset;
}
# Buffer 类
final int nextPutIndex() {                          // package-private
    if (position >= limit)
        throw new BufferOverflowException();
    return position++;
}

3. flip()

    此时直接可以读取缓冲区中的元素么,答案是不可以的。因为目前处于写模式,而不是读模式如果要想读取的话需要转换成读模式,那们我们需要调用filp()方法

public class NioDemo {
    public static void main(String[] args) {
        IntBuffer buffer = IntBuffer.allocate(10);
        for (int i = 1; i <= 5; i++) {
            buffer.put(i);
        }

        for (int i = 0; i < buffer.position(); i++) {
            System.out.print(buffer.get(i) + "|");
        }
        System.out.println("\n"); // 换行
        System.out.println("转换之前读取缓冲区的一个值 : " + buffer.get());
        System.out.println("capacity : " + buffer.capacity());
        System.out.println("limit : " + buffer.limit());
        System.out.println("position : " + buffer.position());
        buffer.flip();
        System.out.println("模式切换......");
        // 切换模式的时候position 变成了0,而limit 变成了之前的position的值
        for (int i = 0; i < buffer.limit() - 1; i++) {
            System.out.print(buffer.get() + "|");
        }
        System.out.println("\n"); // 换行
        System.out.println("capacity : " + buffer.capacity());
        System.out.println("limit : " + buffer.limit());
        System.out.println("position : " + buffer.position());
    }
}

image.png
    切换模式的时候position 变成了0,而limit 变成了之前的position的值,看结果就能看出来,不信我们看一下这个flip的源码,这里不仅limit 和position换了位置,mark也被重置了
源码分析

public final Buffer flip() {
    // 将position的值赋值给limit
    limit = position;
    // position的索引重置为0
    position = 0;
    // mark 变为-1
    mark = -1;
    return this;
}

    看其flip底层的源码就能看出来,这个flip就是这么操作这几个变量的索引的,那么读模式怎么再次变成写模式呢,其实可以调用clear方法。


4. clear()

    该方法就是清除缓冲区的,但是事实上并没有被清除,只是将position、limit、mark这几个值恢复到原始的位置了,这样就可以重头开始往缓冲区中写数据了
源码分析

public final Buffer clear() {
    // 将position的设置为0
    position = 0;
    // limit设置和capacity 大小一致
    limit = capacity;
    // mark 变成了-1
    mark = -1;
    return this;
}

5. get()

public class NioDemo {
    public static void main(String[] args) {
        IntBuffer buffer = IntBuffer.allocate(10);
        for (int i = 1; i <= 5; i++) {
            buffer.put(i);
        }

        for (int i = 0; i < buffer.position(); i++) {
            System.out.print(buffer.get(i) + "|");
        }
        System.out.println("\n"); // 换行
        System.out.println("转换之前读取缓冲区的一个值 : " + buffer.get());
        System.out.println("capacity : " + buffer.capacity());
        System.out.println("limit : " + buffer.limit());
        System.out.println("position : " + buffer.position());
        buffer.flip();
        System.out.println("模式切换......");
        System.out.println("capacity : " + buffer.capacity());
        System.out.println("limit : " + buffer.limit());
        System.out.println("position : " + buffer.position());
        System.out.println("利用get读取缓冲区中的数据");
        // 切换模式的时候position 变成了0,而limit 变成了之前的position的值
        for (int i = 0; i < buffer.limit() - 1; i++) {
            System.out.print(buffer.get() + "|");
        }
        System.out.println("\n"); // 换行
        System.out.println("capacity : " + buffer.capacity());
        System.out.println("limit : " + buffer.limit());
        System.out.println("position : " + buffer.position());
    }
}

image.png
源码分析

public int get() {
    return hb[ix(nextGetIndex())];
}

final int nextGetIndex() { 
    // 每次计算position的索引的时候都必须保证position的位置不能大于等于limit的
    if (position >= limit)
        throw new BufferUnderflowException();
    return position++;
}

    每次计算position的索引的时候都必须保证position的位置不能大于等于limit的,因为limit限制了position可读取的数据范围。
    我们换成读模式之后,利用get方法读取了缓冲区中的数据,此时limit和capacity 没有变化,但是position的值变成了5,此时我们能变成写模式么,不能的,如果直接写的话会抛出异常的,我们看一下put源码
源码分析

public IntBuffer put(int x) {
    hb[ix(nextPutIndex())] = x;
    return this;
}

final int nextPutIndex() {
    // 如果position>= limit的话那么就会抛出异常
    if (position >= limit)
        throw new BufferOverflowException();
    return position++;
}

    此时例子中的 position =5,limit = 6,如果再一次put的话position++,就会造成position==limit 抛出异常。所以此时我们想要变成写模式的话必须还得调用一次clear方法。那么缓冲区的内容可以重复的读取么,答案是可以的,我们可以调用rewind方法,或者通过mark 和reset方法进行配合实现,我们先看rewind方法


6. rewind()

    已经读完的缓冲区,如果需要再次读取一遍的话可以调用rewind方法,我们看一下为什么不能二次遍历先

public class NioDemo {
    public static void main(String[] args) {
        IntBuffer buffer = IntBuffer.allocate(10);
        for (int i = 1; i <= 5; i++) {
            buffer.put(i);
        }
        buffer.flip();
        System.out.println("一次遍历......");
        for (int i = 0; i < buffer.limit(); i++) {
            System.out.print(buffer.get() + "|");
        }
        System.out.println();
        System.out.println("二次遍历......");
        for (int i = 0; i < buffer.limit(); i++) {
            System.out.print(buffer.get() + "|");
        }
    }
}

image.png
    第二次遍历直接抛出异常,因为此时的position已经到达了limit的前一个位置了,再读取的话position就和limit相等了,此时就会抛出异常了。所以我们调用rewind方法再试一次

public class NioDemo {
    public static void main(String[] args) {
        IntBuffer buffer = IntBuffer.allocate(10);
        for (int i = 1; i <= 5; i++) {
            buffer.put(i);
        }
        buffer.flip();
        System.out.println("一次遍历......");
        for (int i = 0; i < buffer.limit(); i++) {
            System.out.print(buffer.get() + "|");
        }
        buffer.rewind();
        System.out.println();
        System.out.println("二次遍历......");
        for (int i = 0; i < buffer.limit(); i++) {
            System.out.print(buffer.get() + "|");
        }
    }
}

image.png
    成功再一次进行了遍历,那我们再看看rewind底层源码,其实我感觉还是这个几个值的变化
源码分析

public final Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
}

    果真如此,limit 不用变,只是把mark变成了-1,position变成了0,使其重头开始


7.mark 和 reset方法

    这两个方法是配套使用的方法,mark方法就是将当前的position的值赋值给mark,让mark记住这个位置,然后再调用reset方法,让position等于其mark中保存的position。

public class NioDemo {
    public static void main(String[] args) {
        IntBuffer buffer = IntBuffer.allocate(10);
        for (int i = 1; i <= 5; i++) {
            buffer.put(i);
        }
        buffer.flip();
        System.out.println("在第一次遍历中,将第三个元素用mark方法标记一下");
        for (int i = 0; i < buffer.limit(); i++) {
            if (i == 2) {
                buffer.mark();
            }
            System.out.print(buffer.get() + "|");
        }
        System.out.println();
        System.out.println("调用reset方法之前...... : " + buffer.position());
        buffer.reset();
        System.out.println("调用reset方法之后...... : " + buffer.position());

        System.out.println("第二次遍历......");
        int count = buffer.limit() - buffer.position();
        for (int i = 0; i < count; i++) {
            System.out.print(buffer.get() + "|");
        }
    }
}

image.png
    显然两个方法的配套使用想让posititon从哪里开始遍历就从哪里遍历,看一下mark方法源码
源码分析

public final Buffer mark() {
    mark = position;
    return this;
}

    其实 mark 就是记录了一下position的当前位置,再看一下reset方法

public final Buffer reset() {
    int m = mark;
    if (m < 0)
        throw new InvalidMarkException();
    position = m;
    return this;
}

    其实reset方法就是将之前mark保存的position值赋值给了当前position


使用步骤

  1. 第一步:

通过allocate方法申请一个缓冲区,刚创建的时候属于写模式

  1. 第二步:

同过put方法往缓冲区中写数据

  1. 第三步:

通过flip方法将写模式变成读模式

  1. 第四步:

利用get方法读取数据

  1. 第五步:

读取之后再通过调用clear方法可以再一次进行写数据,也就是切换成写模式

总结

    其实Buffer缓冲区的操作只是操作capacity、limit、position这几个值,比大小等操作,真正看了这些方法的源码之后就明白了其中的工作原理,并没有想象中的那么复杂,后续更新 NIO 基础组件之Selector