Java NIO之Buffer的使用

bmilk發表於2020-07-02

目錄

  • Buffer簡介
  • Buffer的核心屬性
  • Buffer的建立與使用(ByteBuffer為例)
  • 總結
  • 參考資料

Buffer簡介

緩衝區(Buffer):本質上是一個陣列,用於臨時儲存、寫入以及讀取資料。在Java NIO中,
該記憶體塊包含在NIO Buffer物件當中,NIO Buffer物件還提供了一組介面來訪問該記憶體塊。

根據資料型別的不同,Java為除了boolean型別之外的其餘7種基本型別提供了相應型別的緩衝區,
分別是ByteBufferCharBufferShortBufferIntBufferLongBuffer
FloatBufferDoubleBuffer。他們都繼承自抽象類Buffer類,他們的管理方式也都幾乎一樣。
UML類圖如下:

Buffer的核心屬性

BUffer類的部分實現如下:

public abstract class Buffer {
    // Invariants: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;

    //構造方法
    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;
        }
    }
    
    /**
     * Returns this buffer's capacity.
     *
     * @return  The capacity of this buffer
     */
    //返回這個Buffer的容量
    public final int capacity() {
        return capacity;
    }

    /**
     * Returns this buffer's position.
     *
     * @return  The position of this buffer
     */
    //返回這個Buffer中當前的位置(當前運算元)
    public final int position() {
        return position;
    }

    /**
     * Returns this buffer's limit.
     *
     * @return  The limit of this buffer
     */
    //返回當前Buffer中可以被操作的元素的個數
    public final int limit() {
        return limit;
    }

    /**
     * Sets this buffer's mark at its position.
     *
     * @return  This buffer
     */
    //記錄當前position的位置
    public final Buffer mark() {
        mark = position;
        return this;
    }
    
    public final Buffer reset() {
        int m = mark;
        if (m < 0)
            throw new InvalidMarkException();
        position = m;
        return this;
    }

}

其中定義了四個Buffer屬性,對應的描述如下

屬性 描述
capacity 容量;用於描述這個Buffer大小,即建立的陣列的長度,一旦宣告不可以被改變
position 位置,表示當前緩衝區中正在操作的資料的位置,在切換讀取時會將其置0
limit 界限、限制;表示當前緩衝區中可以操作的資料的大小,預設情況下為Buffer的大小,切換為讀取模式後為陣列中元素的個數(準確的說時切換之前position的值)
mark 標記;用於記錄當前position的位置,後續操作過程中可以使用reset()方法將position還原至最後一次mark的位置

Buffer的建立與使用(ByteBuffer為例)

Buffer的建立

Java NIO中可以使用對應Buffer類的allocate()或者allocateDirect()靜態方法建立。

//使用allocate()建立
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);

//使用allocateDirect()建立
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);

Buffer的本質是一個陣列,建立時需要指定陣列的大小

Buffer的使用

Buffer的使用一般分為四個步驟

  1. Buffer中寫入資料
  2. Buffer切換為讀取模式
  3. 讀取Buffer
  4. Buffer清空,供後續寫入使用

1. 寫如資料

//使用put()方法向Buffer中寫入資料
byteBuffer.put("bmilk".getBytes());

//使用Channel#read()向Buffer中寫入資料
channel.read(byteBuffer);

2. 將Buffer切換為讀取模式

可以通過呼叫flip()方法將Buffer從寫模式切換到讀模式。

byteBuffer.flip()

呼叫flip()方法會將position設回0,並將limit設定成之前position的值。
即,現在使用position標記讀的位置,limit表示之前寫進了多少個byte,也就是現在
能讀取多少個byte等。

3. 讀取Buffer
讀取Buffer有兩種方式:

  1. Buffer種讀取資料到Channel
  2. 使用get()方法從Buffer種讀取資料
//從Buffe中將資料寫入通道
inChannel.write(byteBuffer)

//使用get()方法從BUffer中讀取資料
byte[] bytes=new byte[byteBuffer.limit()];
byteBuffer.get(bytes);

4. 將Buffer清空,供後續寫入使用
使用clear()清空緩衝區,清空緩衝區只是使各個指標恢復初始位置,
更具體的說是position設定為0,limit設定為容量的初始大小。
並不會真實清空其中資料,但是可以通過後續的寫覆蓋之前的資料

byteBuffer.clear()

其他的一些方法

  1. 使用rewind()Buffer重複讀取資料
//使用`rewind()`從`Buffer`重複讀取資料
//Buffer.rewind()將position設回0,所以你可以重讀Buffer中的所有資料。
//limit保持不變,仍然表示能從Buffer中讀取多少個元素(byte、char等)。
Buffer rewind = byteBuffer.rewind();
  1. compact()方法

clear()會使使各個指標恢復初始位置,但是實際中可能存在部分資料還沒有被使用,而後續需要使用。
又必須清理一部分Buffer的空間,compact()方法會將所有未讀資料拷貝到Buffer的起始處,
然後將position指標設定到最後一個未讀元素的後面,現在Buffer可以進行寫資料,
但是不會覆蓋前面的未讀的資料。

  1. mark()方法與reset()方法

通過呼叫Buffer.mark()方法,可以標記Buffer中的當前的position。之後可以通過呼叫Buffer.reset()方法恢復到這個position。

//使用mark標記當前的position位置
byteBUffer.mark()
//使用reset方法使position指標返回這個位置
byteBuffer.reset()

4.equals()方法與compareTo()方法

當需要比較兩個Buffer時可以使用equals()方法與compareTo()方法。

equals()方法判斷兩個方式是否相等,當滿足下列條件時,表示兩個Buffer相等

  • 有相同的型別(bytecharint等)
  • Buffer中剩餘的bytechar等的個數相等。
  • \(\color{#FF3030}{`Buffer`中所有剩餘的`byte`、`char`等都相同}\)

compareTo()方法比較兩個兩個Buffer的大小,僅比較剩餘元素(bytechar等)
如果滿足下列條件,則認為一個Buffer“小於”另一個Buffer

  • 第一個不相等的元素小於另一個Buffer中對應的元素
  • 所有元素都相等,但第一個Buffer比另一個先耗盡(第一個Buffer的元素個數比另一個少)。

直接緩衝區與非直接緩衝區

  • 非直接緩衝區:通過allocate()方法分配緩衝區,將緩衝區建立在JVM記憶體中
  • 直接俄緩衝區:通過allocateDirect()方法分配直接緩衝區,將緩衝區建立在實體記憶體中,可以在某些情況下提高效率

非直接緩衝區

  • 非直接緩衝區資料流向圖

直接緩衝區

  • 直接緩衝區資料流向圖

直接緩衝區(實體記憶體對映檔案):相比非直接緩衝區省略了copy的過程,所以說直接緩區可以一定程度上提高效率

弊端:

  • 開闢空間時資源消耗大
  • 不安全,java程式將資料寫入實體記憶體對映檔案中,之後資料將不受Java程式控制,
    什麼時候寫入硬碟無法控制(由作業系統控制),當垃圾回收機制釋放引用後才能斷開與之的連線

小結

  • 緩衝區要麼是直接的,要麼是非直接的如果為直接位元組緩衝區,則java虛擬機器會見最大努力直接在此緩衝區上執行本機I/O
    也就是說,每次呼叫基礎作業系統的I/O之前或之後,虛擬機器都回儘量避免將緩衝區的內容複製到中間緩衝區或者從中間緩衝區中複製內容。
  • 直接位元組緩衝區可以通過呼叫此類的allocateDirect()工廠方法來建立,
    此方法返回的緩衝區進行分配和取消分配所需的程本通常高於非直接緩衝區,
    直接緩衝區的內容可以駐留在常規的垃圾回收堆之外,因此他們對應用程式記憶體需求造成的影響可能並不明顯,
    所以建議直接緩衝區主要分配給易受基礎系統的本機I/O操作影響的大型、持久得緩衝區。
    一般情況下,最好盡在直接緩衝區能在程式效能方面帶來明顯好處時分配他們。
  • 直接位元組緩衝區還可以通過FileChannelmap()方法,將檔案區域直接對映到記憶體中來建立,
    該方法返回MappedByteBufferJava的實現有助於JNI從本地及程式碼建立直接位元組緩衝區,
    如果以上這些緩衝區中的某個緩衝區例項指的是不可訪問的記憶體區域。
    則試圖訪問該區域不會更改緩衝區的內容,並且將會在訪問期間或稍後的時間導致丟擲不確定的異常
  • 位元組緩衝區是直接緩衝區還是非直接緩衝區可以通過呼叫其isDirect()方法來確定,提供此方法是為了能夠在效能關鍵型程式碼中執行顯式緩衝區管理。

總結

本文簡單介紹了Buffer的種類,並對常用方法進行樂簡單的介紹

參考資料

Java NIO系列教程(三) Buffer

相關文章