Java进阶之从ByteBuffer到设计缓存

最近在忙hadoop-common项目的国密算法适配,其中有涉及到一个基本问题引起我的注意,就是bytebuffer和byte[]的转化,网上的资料太繁杂,而且大多数感觉没讲清楚,我在这里做下整理。而且想到面试的时候的一道经典面试题就是如何设计缓存,正好借着这个机会一起归总一下。这篇应该门槛不高,不过也需要一些java nio和缓存的基础,所以放到后台里面了。

一,什么是ByteBuffer?

关于缓存

首先简单提一下什么是缓存,有这么一个例子:

从硬盘disk到内存memory之间有很长一段路要走,我们要把数据从disk搬到memory的消耗会很大,这严重影响了计算机的性能

                         disk ------------------------------------------------------> memory

这个时候,如果想提高效率,就要在中途设计一个缓存buffer来减少搬运的消耗,需要的话我可以直接从buffer中取,像这样

                         disk------------------------------------->buffer----------->memory

当然,这只是一个简单的模型,具体Java多线程的场景缓存的用法会更复杂一些,这里不再赘述。

Java为8种类型都提供了自己的buffer类型,这里讲最常用的ByteBuffer,ByteBuffer在java.nio这个包中继承了buffer类。学习java最好的方式是源码,我们就从源码入手来剖析ByteBuffer到底是什么,

ByteBuffer种类

首先,ByteBuffer分为direct直接读写buffer和non-direct非直接读写的buffer。了解这个其实和ByteBuffer的底层架构有关系:

                                                  

                                                               fig.1 ByteBuffer继承结构

这里边的HeapByteBuffer是在jvm堆上面的一个buffer,底层的本质是一个数组,由于内容维护在jvm里,所以把内容写进buffer里速度会快些;并且,可以更容易回收,外设读取jvm堆里的数据时,不是直接读取的,而是把jvm里的数据读到一个内存块里,再在这个块里读取的;

而ByteBuffer也可以选择支持通过JNI从本机代码创建直接字节缓冲区,DirectByteBuffer的底层的数据其实是维护在操作系统的内存中,而不是jvm里,DirectByteBuffer里可以通过将文件的区域直接映射到内存中来创建,使用directByteBuffer跟外设(IO设备)打交道时会快很多,可以省去读取内存块这一步,实现零拷贝。

ByteBuffer可以通过isDirect()方法返回值来判定是否是direct型的。

这里省略讲解Java基于ByteBuffer的大小端转换来访问二进制数据。

ByteBuffer属性及构造方法

// Creates a new buffer with the given mark, position, limit, capacity,
    // backing array, and array offset
    //
    ByteBuffer(int mark, int pos, int lim, int cap,   // package-private
                 byte[] hb, int offset)
    {
        super(mark, pos, lim, cap);
        this.hb = hb;
        this.offset = offset;
    }

    // Creates a new buffer with the given mark, position, limit, and capacity
    //
    ByteBuffer(int mark, int pos, int lim, int cap) { // package-private
        this(mark, pos, lim, cap, null, 0);
    }

byte[] buff  //buff即内部用于缓存的数组。
position //position类似于读写指针,表示当前读(写)到什么位置
mark //为某一读过的位置做标记,便于某些时候回退到该位置。可以用于mark()/reset()
capacity //初始化时候的容量。Capacity在读写模式下都是固定的,就是我们分配的缓冲大小。
limit //当写数据到buffer中时,limit一般和capacity相等,在读模式下表示最多能读多少数据,此时和缓存中的实际数据大小相同。
offset //当前数据的偏移

基本操作

put
写模式下,往buffer里写一个字节,并把postion移动一位。写模式下,一般limit与capacity相等。


flip
写完数据,需要开始读的时候,将postion复位到0,并将limit设为当前postion。也就是说调用flip之后,读写指针指到缓存头部,并且设置了最多只能读出之前写入的数据长度(而不是整个缓存的容量大小)。


get
从buffer里读一个字节,并把postion移动一位。上限是limit,即写入数据的最后位置。

clear

将position置为0,并不清除buffer内容。

常规方法

ByteBuffer allocate(int capacity)             创建一个指定capacity的ByteBuffer。
ByteBuffer allocateDirect(int capacity)  创建一个direct的ByteBuffer,这样的ByteBuffer在参与IO操作时性能会更好
ByteBuffer wrap(byte [] array)
ByteBuffer wrap(byte [] array, int offset, int length)   把一个byte数组或byte数组的一部分包装成ByteBuffer。新buffer的capacity是array的长度,offset=postion,mark未定义,limit和原定义一样
byte get(int index)
ByteBuffer put(byte b)
int getInt()               从ByteBuffer中读出当前position标志的下4个字节组成一个int值。
ByteBuffer putInt(int value)     写入一个int值到ByteBuffer中。get和put其他类型的以此类推

特殊方法

Buffer clear()    把position设为0,把limit设为capacity,一般在把数据写入Buffer前调用。
Buffer flip()    把limit设为当前position,把position设为0,一般在从Buffer读出数据前调用。
Buffer rewind()  把position设为0,limit不变,一般在把数据重写入Buffer前调用。
compact()       将 position 与 limit之间的数据复制到buffer的开始位置,复制后 position = limit -position,limit = capacity, 但如         果position 与limit 之间没有数据的话发,就不会进行复制。
mark() & reset()     通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。

同时ByteBuffer还提供了如下所示的链式调用以及和其他类型buffer转换的接口

bb.putInt(0xCAFEBABE).putShort(3).putShort(45);

二,ByteBuffer到底怎么和byte[]互转?

如果第一节的东西领会了,就知道我们现在面对的这个问题其实就是一个调用buffer读写的问题,解决方法也显而易见。拿密码的加解密做例子:

  // byte[]转化成ByteBuffer
  public ByteBuffer encodeValue(byte[] value) {
    ByteBuffer byteBuffer = ByteBuffer.wrap(value);
    return byteBuffer;
  }

  // ByteBuffer转化成byte[]
  public byte[] decodeValue(ByteBuffer bytes) {
    int len = bytes.limit() - bytes.position();
    byte[] bytes1 = new byte[len];
    bytes.get(bytes1);
    return bytes1;
  }

byte[]转化成ByteBuffer,直接使用ByteBuffer封装好的wrap方法;Buffer转成byte[],先确定好我们需要的数组长度,然后调用get方法。

又上边的ByteBuffer转byte[],我们可以再思考一个问题,怎么判断两个ByteBuffer中的值相等?

ByteBuffer中的equals方法是这么写的:

    /* <p> A byte buffer is not equal to any other type of object.  </p>
     *
     * @param  ob  The object to which this buffer is to be compared
     *
     * @return  <tt>true</tt> if, and only if, this buffer is equal to the
     *           given object
     */
    public boolean equals(Object ob) {
        if (this == ob)
            return true;
        if (!(ob instanceof ByteBuffer))
            return false;
        ByteBuffer that = (ByteBuffer)ob;
        if (this.remaining() != that.remaining())
            return false;
        int p = this.position();
        for (int i = this.limit() - 1, j = that.limit() - 1; i >= p; i--, j--)
            if (!equals(this.get(i), that.get(j)))
                return false;
        return true;
    }

其实就是通过position和limit指针,比较缓存remaining元素是否相等,把remaining部分的byte一个一个取出比较判断。

由此我的问题解决了,操作buffer类的对象时要想到buffer是怎么读怎么写,怎么调用方法的,才能合理正确的使用和应对buffer带来的问题,不能只看方法名自己去臆想。

三,如何设计缓存?

那么,我们再把问题升华一下,看了这么多我们应该如何设计缓存呢?

起初我的想法是设计缓存=选择合理的数据结构执行读写操作。了解过Java实现的ByteBuffer之后感觉了解要更深刻而去buffer设计绝不止于此。我们在这抽取关键信息,整理一下设计怎么设计缓存的思路。

1. 首先我们要选择合适的缓存属性和构造方法

为了配合读写位置的控制,要有读写指针,通常的实现是有一个读指针,有一个写指针。每次要解决它们相遇问题重新调整或抛异常。现在我们可以仿造java的buffer进行读写的切换,最简单构造一个position指针和limit指针就好了。

2. 然后我们要满足buffer的基本需求就是要实现数据的读写,落到实现上就是get/put方法

数据可以存在内存中的数据结构,你可以直接调用数据结构封装好的get和put方法。如果能力可以的话,可以仿造java的direct buffer和JVM buffer实现底层一点,粘个例子:

    public ByteBuffer get(byte[] 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);
        return this;
    }

不过一般的话,你也要考虑读写过程中的问题,比如检查边界,调整position,抛出异常等。

3. 这还没完,有了读写方法,还要考虑读写的切换,ByteBuffer的主体结构是

allocate -> 分配capacity(可以使用构造方法)

flip -> 写换读 limit=position position=0

rewind -> 读换写 position = 0

clear -> 指针归位

以上就可以完成一个简单的缓存设计了。

 

参考资料:

1. https://docs.oracle.com/javase/8/docs/api/java/nio/ByteBuffer.html

2. https://www.jianshu.com/p/ebc52832dca0

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章