0
点赞
收藏
分享

微信扫一扫

NIO 基础组件之 Buffer



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


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

NIO 基础组件之 Buffer_Java
    目前这个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());
}
}

NIO 基础组件之 Buffer_后端_02
    往缓冲区中写入了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());
}
}

NIO 基础组件之 Buffer_源码分析_03
    切换模式的时候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());
}
}

NIO 基础组件之 Buffer_后端_04
源码分析

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() + "|");
}
}
}

NIO 基础组件之 Buffer_源码分析_05
    第二次遍历直接抛出异常,因为此时的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() + "|");
}
}
}

NIO 基础组件之 Buffer_数据_06
    成功再一次进行了遍历,那我们再看看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() + "|");
}
}
}

NIO 基础组件之 Buffer_源码分析_07
    显然两个方法的配套使用想让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

举报

相关推荐

0 条评论