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

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