详解,NIO中的缓冲区

  • J3 - 白起
  • 技术(NIO # 缓冲区)

NIO 的出现就是为了解决传统 IO 上的不足,而 NIO 三大组件中的缓冲区就是提高效率的组件之一。

在 NIO 中缓冲区是占据着非常重要的地位,因为数据就放在缓冲区中,对数据的 CRUD 操作都是对缓冲区的操作,所以缓冲区操作的对于否都直接关系到最终结果的正确性。

下面就开始了解它把!

一、缓冲区类体系介绍

JDK1.4 抽象出了一个缓冲区的抽象类,该类是顶级类下面有七个直接子类,如图:

在这里插入图片描述

对于我们 Java 八种基本数据类型中,除了 boolean 没有对应的缓冲类,其余七种都有,而且使用最多的就是 ByteBufferCharBuffer

抽象类即为模板,子类都是按照父类的规范做事,既可以用父类实现的也可以自己重写,所以先看看 Buffer 类。

二、Buffer 类使用

该类为抽象类不能直接实例化,而其七个子类同样也是抽象类,那我们如何使用?

使用子类提供的静态方法返回的包装类进行实现:wrap、allocate 等。

Buffer 类的 API 列表如图:

在这里插入图片描述

对于这些 API 我要重点说说标红区域的四个属性,它可以说是缓冲区的核心了。

// 标记
private int mark = -1;
// 位置
private int position = 0;
// 限制
private int limit;
// 容量
private int capacity;

2.1 四个核心属性

Buffer 源码中有说明四个属性的大小关系:

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

1)容量:capacity

这个好理解,表示的就是缓冲区的大小,即缓冲区可以存放元素的数量。capacity 是不能为负数的且定义之后不可更难改。

int capacity() // 返回此缓冲区大小

看一下案例:

@Test
public void test04() {
    byte[] b = new byte[]{1, 2, 3, 4, 5};
    char[] c = new char[]{'a', 'b', 'c', 'd', 'e'};
    ByteBuffer byteBuffer = ByteBuffer.wrap(b);
    CharBuffer charBuffer = CharBuffer.wrap(c);
    // 其余类型buffer类似,就不一一演示
    log.info("ByteBuffer实现类型:{}", byteBuffer.getClass().getName());
    log.info("CharBuffer实现类型:{}", charBuffer.getClass().getName());
    log.info("===========================");
    log.info("ByteBuffer容量capacity:{}", byteBuffer.capacity());
    log.info("CharBuffer容量capacity:{}", charBuffer.capacity());
}
// 结果
/*
ByteBuffer实现类型:java.nio.HeapByteBuffer
CharBuffer实现类型:java.nio.HeapCharBuffer
===========================
ByteBuffer容量capacity:5
CharBuffer容量capacity:5
*/

从结果可以看到,ByteBuffer 类的实现是 HeapByteBuffer,那么其他类型的 Buffer 应该也有对应的 HeapXXXBuffer 类实现。

在这里插入图片描述

继续深入一下 wrap 方法源码

// 1)java.nio.ByteBuffer # wrap
public static ByteBuffer wrap(byte[] array) {
    return wrap(array, 0, array.length);
}
// 2)
public static ByteBuffer wrap(byte[] array,
                              int offset, int length)
{
    try {
        // 最终 new 一个 HeapByteBuffer 实现类
        return new HeapByteBuffer(array, offset, length);
    } catch (IllegalArgumentException x) {
        throw new IndexOutOfBoundsException();
    }
}

// 3)java.nio.HeapByteBuffer # HeapByteBuffer
HeapByteBuffer(byte[] buf, int off, int len) { // package-private
    // 调用父类构造方法,初始化参数
    super(-1, off, off + len, buf.length, buf, 0);
    /*
        hb = buf;
        offset = 0;
        */
}
// 4)java.nio.ByteBuffer # ByteBuffer
ByteBuffer(int mark, int pos, int lim, int cap,   // package-private
           byte[] hb, int offset)
{
    super(mark, pos, lim, cap);
    // 传入的 byte 数组封装到内部
    this.hb = hb;
    this.offset = offset;
}

// 5)java.nio.Buffer # Buffer
Buffer(int mark, int pos, int lim, int cap) {       // package-private
    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;
    }
}

从源码可以看出,通过 wrap 获得的 buffer 其容量就是传入的数组长度也即 capacity 的值。

在这里插入图片描述

2)限制:limit

限制缓冲区某一个位置不可读或写。说白一点就是写入数据或读取数据时,到达 limit 所指引的下标处就会停止后续的读与写操作。

limit 不能大于 capacity ,不能为负数并且 position 如果大于 limit 那么则会将 position 设为新的 limit。

定义的 mark 不能大于 limit 否则会丢弃该 mark。

后面会陆续介绍出现的 position 和 mark 属性,下面看张图了解 limit 作用:

在这里插入图片描述

看个案例:

@Test
public void test05() {
    byte[] b = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    ByteBuffer byteBuffer = ByteBuffer.wrap(b);
    log.info("容量capacity,{}", byteBuffer.capacity());
    log.info("限制limit,{}", byteBuffer.limit());
    log.info("可操作范围:{}", byteBuffer.limit() - byteBuffer.position());
    log.info("获取可操作范围数据(下标 8),{}", byteBuffer.get(8));
    log.info("限制下标7以后都不可操作");
    byteBuffer.limit(7);
    log.info("获取可操作范围数据(下标 5),{}", byteBuffer.get(5));
    log.info("获取不可操作范围数据(下标 8),{}", byteBuffer.get(8));
}

结果:

在这里插入图片描述

很明显,操作 limit 限制后的位置时会报 java.lang.IndexOutOfBoundsException(下标越界异常) ,所以对于缓冲区,我们能操作的只有 position 与 limit 之间的数据。

3)位置:position

这个属性表示下一个要读取的位置。同样 position 不能为负数且不能大于 limit,如果 mark 已经定义且大于 position 那么会丢弃 mark。

在这里插入图片描述

小小案例:

@Test
public void test06() {
    char[] c = new char[]{'a', 'b', 'c', 'd', 'e', 'f', 'g'};
    CharBuffer charBuffer = CharBuffer.wrap(c);
    log.info("当前开始读取下标:{}", charBuffer.position());
    log.info("当前开始读取下标的数据:{}", charBuffer.get());
    log.info("读取下标为5的数据:{}", charBuffer.get(5));
    log.info("下一个要读取的下标:{}", charBuffer.position());
}

结果

13:08:43.279 [main] INFO cn.baiqi.core.nio.TestByteBuffer - 当前开始读取下标:0
13:08:43.285 [main] INFO cn.baiqi.core.nio.TestByteBuffer - 当前开始读取下标的数据:a
13:08:43.285 [main] INFO cn.baiqi.core.nio.TestByteBuffer - 读取下标为5的数据:f
13:08:43.285 [main] INFO cn.baiqi.core.nio.TestByteBuffer - 下一个要读取的下标:1

由案例可以看出 position 的值只有挨个读取的时候才会累加。

4)标记:mark

对 position 的某一位置做标记,当执行 reset 方法时 不论 position 指向何处,都会变原先mark标记的位置。

由此可知,position 和 limit 要大于等于 mark 反之 mark 会无效且不可为负数。

在这里插入图片描述

小小案例:

@Test
public void test02() {
    char[] c = new char[]{'a', 'b', 'c', 'd', 'e'};
    CharBuffer charBuffer = CharBuffer.wrap(c);
    log.info("容量capacity = {}", charBuffer.capacity());
    log.info("起始限制limit = {}", charBuffer.limit());
    log.info("起始读取位置position = {}", charBuffer.position());
    log.info("设置 position 位置为:2");
    charBuffer.position(2);
    // 当前位置做标记
    charBuffer.mark();
    log.info("我标记了 mark 的位置 mark = {},position位置:{}", charBuffer.mark(), charBuffer.position());
    charBuffer.position(4);
    log.info("往前走几步position = {}", charBuffer.position());
    // 回到标记位置
    log.info("执行 reset 方法");
    charBuffer.reset();
    log.info("当前读取位置position = {}", charBuffer.position());
    log.info("position 又回到当初 mark 的位置");
}

结果:

13:18:56.178 [main] INFO cn.baiqi.core.nio.TestByteBuffer - 容量capacity = 5
13:18:56.185 [main] INFO cn.baiqi.core.nio.TestByteBuffer - 起始限制limit = 5
13:18:56.185 [main] INFO cn.baiqi.core.nio.TestByteBuffer - 起始读取位置position = 0
13:18:56.185 [main] INFO cn.baiqi.core.nio.TestByteBuffer - 设置 position 位置为:2
13:18:56.185 [main] INFO cn.baiqi.core.nio.TestByteBuffer - 我标记了 mark 的位置 mark = cde,position位置:2
13:18:56.185 [main] INFO cn.baiqi.core.nio.TestByteBuffer - 往前走几步position = 4
13:18:56.186 [main] INFO cn.baiqi.core.nio.TestByteBuffer - 执行 reset 方法
13:18:56.186 [main] INFO cn.baiqi.core.nio.TestByteBuffer - 当前读取位置position = 2

这种 mark 标记正好可以让程序重复的读取莫一段数据,只需执行 reset 方法即可。

在 buffer 中基本所有的 API 方法的实现都是依据这四个属性的,下面会介绍一些常用方法也会跟进源码里面搂一眼这四个属性的具体体现,但在这之前如果这四个属性还没有在脑海中形成印象的话,建议再过一遍。

三、常用方法

具体可以看 Buffer 源码中的方法,这里只做几个介绍。

3.1 remaining()

作用:剩余空间大小,就是返回 position 与 limit 之间的元素个数。

源码:

public final int remaining() {
    return limit - position;
}

案例:

@Test
public void remainingTests() {
    ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[]{1, 2, 3, 4, 5, 6, 7, 8});
    log.info("当前容量:{}", byteBuffer.capacity());
    log.info("当前位置:{}", byteBuffer.position());
    log.info("可操作空间:{}", byteBuffer.remaining());
    log.info("读取两个数据");
    log.info("{},{}", byteBuffer.get(), byteBuffer.get());
    log.info("剩余可操作空间:{}", byteBuffer.remaining());
}

结果:

13:53:53.389 [main] INFO cn.baiqi.core.nio.BufferTest - 当前容量:8
13:53:53.394 [main] INFO cn.baiqi.core.nio.BufferTest - 当前位置:0
13:53:53.394 [main] INFO cn.baiqi.core.nio.BufferTest - 可操作空间:8
13:53:53.394 [main] INFO cn.baiqi.core.nio.BufferTest - 读取两个数据
13:53:53.395 [main] INFO cn.baiqi.core.nio.BufferTest - 1,2
13:53:53.395 [main] INFO cn.baiqi.core.nio.BufferTest - 剩余可操作空间:6

3.2 hasRemaining()

作用:判断当前位置与限制位置是否有剩余元素。

源码:

public final boolean hasRemaining() {
    return position < limit;
}

案例:

@Test
public void hasRemainingTests() {
    ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[]{1, 2, 3, 4, 5, 6, 7, 8});
    log.info("是否可操作:{}", byteBuffer.hasRemaining());
    log.info("设置position = limit");
    byteBuffer.position(byteBuffer.limit());
    log.info("是否可操作:{}", byteBuffer.hasRemaining());
}

结果:

14:11:35.993 [main] INFO cn.baiqi.core.nio.BufferTest - 是否可操作:true
14:11:35.999 [main] INFO cn.baiqi.core.nio.BufferTest - 设置position = limit
14:11:36.000 [main] INFO cn.baiqi.core.nio.BufferTest - 是否可操作:false

3.3 flip()

作用:反转缓冲区,可以理解为从写变成读

源码:

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

案例:

@Test
public void flipTests() {

    ByteBuffer byteBuffer = ByteBuffer.allocate(10);
    log.info("向缓冲区中写数据");
    byteBuffer.put((byte) 1);
    byteBuffer.put((byte) 2);
    byteBuffer.put((byte) 3);
    byteBuffer.put((byte) 4);
    log.info("flip调用前:position = {},limit = {}", byteBuffer.position(), byteBuffer.limit());
    log.info("反转缓冲区");
    byteBuffer.flip();
    log.info("flip调用后:position = {},limit = {}", byteBuffer.position(), byteBuffer.limit());
    log.info("开始读取数据");
    log.info("读取:{}", byteBuffer.get());
}

结果:

14:35:56.765 [main] INFO cn.baiqi.core.nio.BufferTest - 向缓冲区中写数据
14:35:56.769 [main] INFO cn.baiqi.core.nio.BufferTest - flip调用前:position = 4,limit = 10
14:35:56.770 [main] INFO cn.baiqi.core.nio.BufferTest - 反转缓冲区
14:35:56.770 [main] INFO cn.baiqi.core.nio.BufferTest - flip调用后:position = 0,limit = 4
14:35:56.770 [main] INFO cn.baiqi.core.nio.BufferTest - 开始读取数据
14:35:56.771 [main] INFO cn.baiqi.core.nio.BufferTest - 读取:1

3.4 rewind()

作用:重绕缓冲区,就是丢弃 mark 标记位(赋为 -1),position 置 0。我的理解就是丢弃标记,至于重复读取数据我更倾向去 flip 方法。

源码:

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

案例:

@Test
public void rewindTests() {
    ByteBuffer byteBuffer = ByteBuffer.allocate(10);
    log.info("向缓冲区中写数据");
    byteBuffer.put((byte) 1);
    byteBuffer.put((byte) 2);
    byteBuffer.put((byte) 3);
    byteBuffer.put((byte) 4);
    log.info("mark标记当前位置");
    byteBuffer.mark();
    log.info("rewind调用前:position = {},mark = {}", byteBuffer.position(), byteBuffer.mark());
    log.info("rewind方法指向");
    byteBuffer.rewind();
    log.info("rewind调用后:position = {},mark = {}", byteBuffer.position(), byteBuffer.mark());
    log.info("开始读取数据");
    log.info("读取:{}", byteBuffer.get());
}

结果:

14:48:02.364 [main] INFO cn.baiqi.core.nio.BufferTest - 向缓冲区中写数据
14:48:02.368 [main] INFO cn.baiqi.core.nio.BufferTest - mark标记当前位置
14:48:02.368 [main] INFO cn.baiqi.core.nio.BufferTest - rewind调用前:position = 4,mark = java.nio.HeapByteBuffer[pos=4 lim=10 cap=10]
14:48:02.370 [main] INFO cn.baiqi.core.nio.BufferTest - rewind方法指向
14:48:02.370 [main] INFO cn.baiqi.core.nio.BufferTest - rewind调用后:position = 0,mark = java.nio.HeapByteBuffer[pos=0 lim=10 cap=10]
14:48:02.370 [main] INFO cn.baiqi.core.nio.BufferTest - 开始读取数据
14:48:02.371 [main] INFO cn.baiqi.core.nio.BufferTest - 读取:1

说明:虽然可以读取数据,但是有数据的下标和没有数据的下标丢失了,对于读取数据来说是不安全的,所以个人理解不是重复读的作用,flip 方法更像重复度。

3.5 reset()

作用:将 position 指向 mark 标记位

源码:

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

案例可以看 2.1 小节中的 mark部分,介绍的非常清楚。

3.6 clear()

作用:清空缓冲区,但只是重置了 position、limit、mark 属性的值,数据还是又因为没有具体属性表明缓冲区中那些是有数据,那些是没有数据所以间接可以理解为清空了缓冲区。

源码:

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

案例:

@Test
public void clearsTests() {
    ByteBuffer byteBuffer = ByteBuffer.allocate(10);
    log.info("向缓冲区中写数据");
    byteBuffer.put((byte) 1);
    byteBuffer.put((byte) 2);
    byteBuffer.put((byte) 3);
    byteBuffer.put((byte) 4);
    log.info("当前缓冲区状态:{}", byteBuffer);
    byteBuffer.clear();
    log.info("调用clear方法后,当前缓冲区状态:{}", byteBuffer);
}

结果:

14:53:53.778 [main] INFO cn.baiqi.core.nio.BufferTest - 向缓冲区中写数据
14:53:53.783 [main] INFO cn.baiqi.core.nio.BufferTest - 当前缓冲区状态:java.nio.HeapByteBuffer[pos=4 lim=10 cap=10]
14:53:53.785 [main] INFO cn.baiqi.core.nio.BufferTest - 调用clear方法后,当前缓冲区状态:java.nio.HeapByteBuffer[pos=0 lim=10 cap=10]

四、ByteBuffer 类使用

顾名思义,它提供一个 byte 类型的缓冲区进行操作数据,其余六个同理。

4.1 创建 ByteBuffer 方式

  • wrap 方法:根据传入数组创建指定内容的缓冲区
  • allocate 方法:更加传入的容量创建指定容量的空缓冲区
  • allocateDirect 方法:和 allocate 一样,不过创建的缓冲区确是直接缓冲区

案例:

public void test01() {
    ByteBuffer wrap = ByteBuffer.wrap(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9});
    ByteBuffer allocate = ByteBuffer.allocate(10);
    ByteBuffer allocateDirect = ByteBuffer.allocateDirect(10);
}

4.2 直接缓冲区与非直接缓冲区

非直接缓冲区数据流向是:硬盘 => JVM的中间缓冲区 => buffer

直接缓冲区数据流向:硬盘 => buffer

在这里插入图片描述

非直接缓冲区的弊端就是大大的降低了软件对数据的吞吐量,提高了内存的占有率,而非直接缓冲区正好解决了这一弊端。

4.3 ByteBuffer 类的实现探究

前面我们说过 ByteBuffer 也是个抽象类,所以它下面必定有实现类,看该类继承关系图:

在这里插入图片描述

  • HeapByteBuffer:读/写 byte 非直接缓冲区。
  • HeapByteBufferR:只读 byte 非直接缓冲区。
  • DirectByteBuffer:读/写 byte 直接缓冲区。
  • DirectByteBufferR:只读 byte 直接缓冲区。

对于如何操作 ByteBuffer 看到这里应该是会了把!至于其中的 API 非常之多,但是一些基本的我在上面都已经通过案例的方式写出来了,后面更多的 API 方法我还是建议直接去看 ByteBuffer 这个类的源码比较好,在此就不一个个方法去介绍了(太多了,介绍不过来)。

五、最后

以上就是本篇所有内容,其中 Buffer 类中的四个属性是重中之重,其实你们看 Buffer 类体系源码也可以知道基本每个方法都是根据这四个属性实现了,真是随处可见。

对于 Buffer 类的子类重点关注两个 ByteBuffer 和 CharBuffer ,这两个是开发中用的比较多的了,至于后面几个到用的时候在具体过来熟悉也不晚。

那 NIO 中的缓冲区内容就告一段了,下面就是 Channel 相关的内容了。

如果我们把 Buffer 看做水流的话那么 Channel 就是运送水的通道,所以对于通道相关的内容也是 NIO 中的一个重点,所以也是重点。

好了,今天的内容到这里就结束了,关注我,我们下期见

查阅或参考资料:

《NIO与Socket编程指南》高洪严著


  • 由于博主才疏学浅,难免会有纰漏,假如你发现了错误或偏见的地方,还望留言给我指出来,我会对其加以修正。

  • 如果你觉得文章还不错,你的转发、分享、点赞、留言就是对我最大的鼓励。

  • 感谢您的阅读,十分欢迎并感谢您的关注。

^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

CSDN:J3 - 白起

掘金:J3-白起

知乎:J3-白起

这是一个技术一般,但热衷于分享;经验尚浅,但脸皮够厚;明明年轻有颜值,但非要靠才华吃饭的程序员。

^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

J3code

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值