一步一图带你深入剖析 JDK NIO ByteBuffer 在不同字节序下的设计实现

本文详细剖析了JDK NIO中的ByteBuffer,从Buffer的顶层抽象设计到具体实现,特别是ByteBuffer在不同字节序下的工作原理。内容涵盖Buffer的属性、核心操作、存储机制、视图、读写模式切换,以及HeapByteBuffer的实现细节,包括字节读写、字节序转换等。文章通过实例解释了大端和小端字节序的区别,并讨论了字节序对Buffer操作的影响。最后,探讨了Buffer如何转换为指定基本类型的Buffer,帮助读者深入理解NIO Buffer的设计与实现。
摘要由CSDN通过智能技术生成

1. JDK NIO 中的 Buffer

在 NIO 没有出现之前,Java 传统的 IO 操作都是通过流的形式实现的(包括网络 IO 和文件 IO ),也就是我们常见的输入流 InputStream 和输出流 OutputStream。

但是 Java 传统 IO 的 InputStream 和 OutputStream 的相关操作全部都是阻塞的,比如我们使用 InputStream 的 read 方法从流中读取数据时,如果此时流中没有数据,那么用户线程就必须阻塞等待。

还有一点就是传统的这些输入输出流在处理字节流的时候一次只能处理一个字节,这样在处理网络 IO 的时候读取 Socket 缓冲区中的数据效率就会很低,而且在操作字节流的时候只能线性的处理流中的字节,不能来回移动字节流中的数据。这样导致我们在处理字节流中的数据的时候就显得不是很灵活。

所以综上所述,Java 传统 IO 是面向流的,流的处理是单向,阻塞的,而且无论是从输入流中读取数据还是向输出流中写入数据都是一个字节一个字节来处理的。通常都是从输入流中边读取数据边处理数据,这样 IO 处理效率就会很低,

基于上述原因,JDK1.4 引入了 NIO,而 NIO 是面向 Buffer 的,在处理 IO 操作的时候,会一次性将 Channel 中的数据读取到 Buffer 中然后在做后续处理,向 Channel 中写入数据也是一样,也是需要一个 Buffer 做中转,然后将 Buffer 中的数据批量写入 Channel 中。这样一来我们可以利用 Buffer 将里面的字节数据来回移动并根据我们想要的处理方式灵活处理。

除此之外,Nio Buffer 还提供了堆外的直接内存和内存映射相关的访问方式,来避免内存之间的来回拷贝,所以即使在传统 IO 中用到了 BufferedInputStream 也还是没办法和 Nio Buffer 相匹敌。

那么接下来就让我们正式进入JDK NIO Buffer 如何设计与实现的相关主题

2. NIO 对 Buffer 的顶层抽象

JDK NIO 提供的 Buffer 其实本质上是一块内存,大家可以把它简单想象成一个数组,JDK 将这块内存在语言层面封装成了 Buffer 的形式,我们可以通过 Buffer 对这块内存进行读取或者写入数据,以及执行各种骚操作。

如下图中所示,Buffer 类是JDK NIO 定义的一个顶层抽象类,对于缓冲区的所有基本操作和基础属性全部定义在顶层 Buffer 类中,在 Java 中一共有八种基本类型,JDK NIO 也为这八种基本类型分别提供了其对应的 Buffer 类,大家可以把这些 Buffer 类当做成对应基础类型的数组,我们可以利用这些基础类型相关的 Buffer 类对数组进行各种操作。

​image.png

在为大家解析具体的缓冲区实现之前,我们先来看下这个缓冲区的顶层抽象类 Buffer 中到底定义规范了哪些抽象操作,具有哪些属性,这些属性分别是用来干什么的?先带大家从总体上认识一下JDK NIO 中的 Buffer 设计。

2.1 Buffer 中的属性

public abstract class Buffer {

    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;
    
             .............
}

首先我们先来介绍下 Buffer 中最重要的这三个属性,后面即将介绍的关于 Buffer 的各种骚操作均依赖于这三个属性的动态变化。

​Buffer中的重要属性

  • capacity:这个很好理解,它规定了整个 Buffer 的容量,具体可以容纳多少个元素。capacity 指针之前的元素均是 Buffer 可操作的空间。

  • position:用于指向 Buffer 中下一个可操作性的元素,初始值为 0。在 Buffer 的写模式下,position 指针用于指向下一个可写位置。在读模式下,position 指针指向下一个可读位置。

  • limit:表示 Buffer 可操作元素的上限。什么意思呢?比如在 Buffer 的写模式下,可写元素的上限就是 Buffer 的整体容量也就是 capacity ,capacity - 1 即为 Buffer 最后一个可写位置。在读模式下,Buffer 中可读元素的上限即为上一次 Buffer 在写模式下最后一个写入元素的位置。也就是上一次写模式中的 position。

  • mark:用于标记 Buffer 当前 position 的位置。这个字段在我们对网络数据包解码的时候非常有用,在我们使用 TCP 协议进行网络数据传输的时候经常会出现粘包拆包的现象,所以为了应对粘包拆包的问题,在解码之前都需要先调用 mark 方法将 Buffer 的当前 position 指针保存至 mark 属性中,如果 Buffer 中的数据足够我们解码为一个完整的包,我们就执行解码操作。如果 Buffer 中的数据不够我们解码为一个完整的包(也就是半包),我们就调用 reset 方法,将 position 还原到原来的位置,等待剩下的网络数据到来。

​Buffer Mark字段.png

在我们理解了 Buffer 中这几个重要属性的含义之后,接下来我们就来看一看 JDK NIO 在 Buffer 顶层设计类中定义规范的那些抽象操作。

2.2 Buffer 中定义的核心抽象操作

本小节中介绍的这几个关于 Buffer 的核心操作均是基于上小节中介绍的那些核心指针的动态调整实现的。

2.2.1 Buffer 的构造

构造 Buffer 的主要逻辑就是根据用户指定的参数来初始化 Buffer 中的这四个重要属性:mark,position,limit,capacity。它们之间的关系为:mark <= position <= limit <= capacity 。其中 mark 初始默认为 -1,position 初始默认为 0。

​Buffer初始化.png

public abstract class Buffer {

    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;

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

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

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

2.2.2 获取 Buffer 下一个可读取位置

当我们在 Buffer 的读模式下,需要从 Buffer 中读取数据时,需要首先知道当前 Buffer 中 position 的位置,然后根据 position 的位置读取 Buffer 中的元素。随后 position 向后移动指定的步长 nb。

​Buffer获取读取位置.png

nextGetIndex() 方法首先获取 Buffer 当前 position 的位置作为 readIndex 返回给用户,然后 position 向后移动一位。这里的步长 nb 默认为1。


    final int nextGetIndex() {                        
        if (position >= limit)
            throw new BufferUnderflowException();
        return position++;
    }

nextGetIndex(int nb) 方法的逻辑和 nextGetIndex() 方法一样,唯一不同的是该方法指定了position 向后移动的步长 nb。

    final int nextGetIndex(int nb) {          
        if (limit - position < nb)
            throw new BufferUnderflowException();
        int p = position;
        position += nb;
        return p;
    }

大家这里可能会感到好奇,为什么会增加一个指定 position 移动步长的 nextGetIndex(int nb) 方法呢?

在《2. NIO 对 Buffer 的顶层抽象》小节的开始,我们介绍了 JDK NIO 中 Buffer 顶层设计体系,除了 boolean 这个基本类型,NIO 为几乎所有的 Java 基本类型定义了对应的 Buffer 类。

​image.png

假如我们从一个 ByteBuffer 中读取一个 int 类型的数据时,我们就需要在读取完毕后将 position 的位置向后移动 4 位。在这种情况下 nextGetIndex(int nb) 方法的步长 nb 就应该指定为 4.

   public int getInt() {
        return getInt(ix(nextGetIndex((1 << 2))));
    }

2.2.3 获取 Buffer 下一个可写入位置

同获取 readIndex 的过程一样,当我们处于 Buffer 的写模式下,向 Buffer 写入数据时,首先也需要获取 Buffer 当前 position 的位置(writeIndex),当写入元素后,position 向后移动指定的步长 nb。

同样的道理,我们可以向 ByteBuffer 中写入一个 int 型的数据,这时候指定的步长 nb 也是 4 。

    final int nextPutIndex() {                        
        if (position >= limit)
            throw new BufferOverflowException();
        return position++;
    }

    final int nextPutIndex(int nb) {                  
        if (limit - position < nb)
            throw new BufferOverflowException();
        int p = position;
        position += nb;
        return p;
    }

2.2.4 Buffer 读模式的切换

当我们在 Buffer 的写模式下向 Buffer 写入数据之后,接下来我们就需要从 Buffer 中读取刚刚写入的数据。由于 NIO 在对 Buffer 的设计中读写模式是混用一个 position 属性,所以我们需要做读模式的切换。

​flip切换读模式.png

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

我们看到 flip() 方法是对 Buffer 中的这四个指针做了一些调整达到了读模式切换的目的:

  1. 将下一个可写入位置 position 作为读模式下的上限 limit。

  2. position设置为 0 。这样使得我们可以从头开始读取 Buffer 中写入的数据。

2.2.5 Buffer 写模式的切换

有读模式的切换肯定就会有对应的写模式切换,当我们在读模式下以将 Buffer 中的数据读取完毕之后,这时候如果再次向 Buffer 写入数据的话,就需要切换到 Buffer 的写模式下。

​clear切换写模式.png

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

我们看到调用 clear() 方法之后,Buffer 中各个指针的状态又回到了最初的状态:

  1. position 位置重新指向起始位置 0 处。写入上限 limit 重新指向了 capacity 的位置。

  2. 这时向 Buffer 中写入数据时,就会从 Buffer

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值