Java NIO 的前生今世 之三 NIO Buffer 詳解

Java NIO Buffer

當咱們須要與 NIO Channel 進行交互時, 咱們就須要使用到 NIO Buffer, 即數據從 Buffer讀取到 Channel 中, 而且從 Channel 中寫入到 Buffer 中.
實際上, 一個 Buffer 其實就是一塊內存區域, 咱們能夠在這個內存區域中進行數據的讀寫. NIO Buffer 實際上是這樣的內存塊的一個封裝, 並提供了一些操做方法讓咱們可以方便地進行數據的讀寫.
Buffer 類型有:segmentfault

  • ByteBuffer數組

  • CharBufferthis

  • DoubleBuffer操作系統

  • FloatBuffer指針

  • IntBuffercode

  • LongBuffer對象

  • ShortBuffer
    這些 Buffer 覆蓋了能從 IO 中傳輸的全部的 Java 基本數據類型.ip

NIO Buffer 的基本使用

使用 NIO Buffer 的步驟以下:內存

  • 將數據寫入到 Buffer 中.ci

  • 調用 Buffer.flip()方法, 將 NIO Buffer 轉換爲讀模式.

  • 從 Buffer 中讀取數據

  • 調用 Buffer.clear() 或 Buffer.compact()方法, 將 Buffer 轉換爲寫模式.

當咱們將數據寫入到 Buffer 中時, Buffer 會記錄咱們已經寫了多少的數據, 當咱們須要從 Buffer 中讀取數據時, 必須調用 Buffer.flip()將 Buffer 切換爲讀模式.
一旦讀取了全部的 Buffer 數據, 那麼咱們必須清理 Buffer, 讓其重新可寫, 清理 Buffer 能夠調用 Buffer.clear() 或 Buffer.compact().
例如:

public class Test {
    public static void main(String[] args) {
        IntBuffer intBuffer = IntBuffer.allocate(2);
        intBuffer.put(12345678);
        intBuffer.put(2);
        intBuffer.flip();
        System.err.println(intBuffer.get());
        System.err.println(intBuffer.get());
    }
}

上述中, 咱們分配兩個單位大小的 IntBuffer, 所以它能夠寫入兩個 int 值.
咱們使用 put 方法將 int 值寫入, 而後使用 flip 方法將 buffer 轉換爲讀模式, 而後連續使用 get 方法從 buffer 中獲取這兩個 int 值.
每當調用一次 get 方法讀取數據時, buffer 的讀指針都會向前移動一個單位長度(在這裏是一個 int 長度)

Buffer 屬性

一個 Buffer 有三個屬性:

  • capacity

  • position

  • limit
    其中 position limit 的含義與 Buffer 處於讀模式或寫模式有關, 而 capacity 的含義與 Buffer 所處的模式無關.

Capacity

一個內存塊會有一個固定的大小, 即容量(capacity), 咱們最多寫入capacity 個單位的數據到 Buffer 中, 例如一個 DoubleBuffer, 其 Capacity 是100, 那麼咱們最多能夠寫入100個 double 數據.

Position

當從一個 Buffer 中寫入數據時, 咱們是從 Buffer 的一個肯定的位置(position)開始寫入的. 在最初的狀態時, position 的值是0. 每當咱們寫入了一個單位的數據後, position 就會遞增一.
當咱們從 Buffer 中讀取數據時, 咱們也是從某個特定的位置開始讀取的. 當咱們調用了 filp()方法將 Buffer 從寫模式轉換到讀模式時, position 的值會自動被設置爲0, 每當咱們讀取一個單位的數據, position 的值遞增1.
position 表示了讀寫操做的位置指針.

limit

limit - position 表示此時還能夠寫入/讀取多少單位的數據.
例如在寫模式, 若是此時 limit 是10, position 是2, 則表示已經寫入了2個單位的數據, 還能夠寫入 10 - 2 = 8 個單位的數據.

例子:

public class Test {
    public static void main(String args[]) {
        IntBuffer intBuffer = IntBuffer.allocate(10);
        intBuffer.put(10);
        intBuffer.put(101);
        System.err.println("Write mode: ");
        System.err.println("\tCapacity: " + intBuffer.capacity());
        System.err.println("\tPosition: " + intBuffer.position());
        System.err.println("\tLimit: " + intBuffer.limit());

        intBuffer.flip();
        System.err.println("Read mode: ");
        System.err.println("\tCapacity: " + intBuffer.capacity());
        System.err.println("\tPosition: " + intBuffer.position());
        System.err.println("\tLimit: " + intBuffer.limit());
    }
}

這裏咱們首先寫入兩個 int 值, 此時 capacity = 10, position = 2, limit = 10.
而後咱們調用 flip 轉換爲讀模式, 此時 capacity = 10, position = 0, limit = 2;

分配 Buffer

爲了獲取一個 Buffer 對象, 咱們首先須要分配內存空間. 每一個類型的 Buffer 都有一個 allocate()方法, 咱們能夠經過這個方法分配 Buffer:

ByteBuffer buf = ByteBuffer.allocate(48);

這裏咱們分配了48 * sizeof(Byte)字節的內存空間.

CharBuffer buf = CharBuffer.allocate(1024);

這裏咱們分配了大小爲1024個字符的 Buffer, 即 這個 Buffer 能夠存儲1024 個 Char, 其大小爲 1024 * 2 個字節.

關於 Direct Buffer 和 Non-Direct Buffer 的區別

Direct Buffer:

  • 所分配的內存不在 JVM 堆上, 不受 GC 的管理.(可是 Direct Buffer 的 Java 對象是由 GC 管理的, 所以當發生 GC, 對象被回收時, Direct Buffer 也會被釋放)

  • 由於 Direct Buffer 不在 JVM 堆上分配, 所以 Direct Buffer 對應用程序的內存佔用的影響就不那麼明顯(實際上仍是佔用了這麼多內存, 可是 JVM 很差統計到非 JVM 管理的內存.)

  • 申請和釋放 Direct Buffer 的開銷比較大. 所以正確的使用 Direct Buffer 的方式是在初始化時申請一個 Buffer, 而後不斷複用此 buffer, 在程序結束後才釋放此 buffer.

  • 使用 Direct Buffer 時, 當進行一些底層的系統 IO 操做時, 效率會比較高, 由於此時 JVM 不須要拷貝 buffer 中的內存到中間臨時緩衝區中.

Non-Direct Buffer:

  • 直接在 JVM 堆上進行內存的分配, 本質上是 byte[] 數組的封裝.

  • 由於 Non-Direct Buffer 在 JVM 堆中, 所以當進行操做系統底層 IO 操做中時, 會將此 buffer 的內存複製到中間臨時緩衝區中. 所以 Non-Direct Buffer 的效率就較低.

寫入數據到 Buffer

int bytesRead = inChannel.read(buf); //read into buffer.
buf.put(127);

從 Buffer 中讀取數據

//read from buffer into channel.
int bytesWritten = inChannel.write(buf);
byte aByte = buf.get();

重置 position

Buffer.rewind()方法能夠重置 position 的值爲0, 所以咱們能夠從新讀取/寫入 Buffer 了.
若是是讀模式, 則重置的是讀模式的 position, 若是是寫模式, 則重置的是寫模式的 position.
例如:

public class Test {
    public static void main(String[] args) {
        IntBuffer intBuffer = IntBuffer.allocate(2);
        intBuffer.put(1);
        intBuffer.put(2);
        System.err.println("position: " + intBuffer.position());

        intBuffer.rewind();
        System.err.println("position: " + intBuffer.position());
        intBuffer.put(1);
        intBuffer.put(2);
        System.err.println("position: " + intBuffer.position());

        
        intBuffer.flip();
        System.err.println("position: " + intBuffer.position());
        intBuffer.get();
        intBuffer.get();
        System.err.println("position: " + intBuffer.position());

        intBuffer.rewind();
        System.err.println("position: " + intBuffer.position());
    }
}

rewind() 主要針對於讀模式. 在讀模式時, 讀取到 limit 後, 能夠調用 rewind() 方法, 將讀 position 置爲0.

關於 mark()和 reset()

咱們能夠經過調用 Buffer.mark()將當前的 position 的值保存起來, 隨後能夠經過調用 Buffer.reset()方法將 position 的值回覆回來.
例如:

public class Test {
    public static void main(String[] args) {
        IntBuffer intBuffer = IntBuffer.allocate(2);
        intBuffer.put(1);
        intBuffer.put(2);
        intBuffer.flip();
        System.err.println(intBuffer.get());
        System.err.println("position: " + intBuffer.position());
        intBuffer.mark();
        System.err.println(intBuffer.get());

        System.err.println("position: " + intBuffer.position());
        intBuffer.reset();
        System.err.println("position: " + intBuffer.position());
        System.err.println(intBuffer.get());
    }
}

這裏咱們寫入兩個 int 值, 而後首先讀取了一個值. 此時讀 position 的值爲1.
接着咱們調用 mark() 方法將當前的 position 保存起來(在讀模式, 所以保存的是讀的 position), 而後再次讀取, 此時 position 就是2了.
接着使用 reset() 恢復原來的讀 position, 所以讀 position 就爲1, 能夠再次讀取數據.

flip, rewind 和 clear 的區別

flip

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

Buffer 的讀/寫模式共用一個 position 和 limit 變量.
當從寫模式變爲讀模式時, 原先的 寫 position 就變成了讀模式的 limit.

rewind

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

rewind, 即倒帶, 這個方法僅僅是將 position 置爲0.

clear

clear 方法源碼:
public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}

根據源碼咱們能夠知道, clear 將 positin 設置爲0, 將 limit 設置爲 capacity.
clear 方法使用場景:

  • 在一個已經寫滿數據的 buffer 中, 調用 clear, 能夠從頭讀取 buffer 的數據.

  • 爲了將一個 buffer 填充滿數據, 能夠調用 clear, 而後一直寫入, 直到達到 limit.

例子:
IntBuffer intBuffer = IntBuffer.allocate(2);
intBuffer.flip();
System.err.println("position: " + intBuffer.position());
System.err.println("limit: " + intBuffer.limit());
System.err.println("capacity: " + intBuffer.capacity());

// 這裏不能讀, 由於 limit == position == 0, 沒有數據.
//System.err.println(intBuffer.get());

intBuffer.clear();
System.err.println("position: " + intBuffer.position());
System.err.println("limit: " + intBuffer.limit());
System.err.println("capacity: " + intBuffer.capacity());

// 這裏能夠讀取數據了, 由於 clear 後, limit == capacity == 2, position == 0,
// 即便咱們沒有寫入任何的數據到 buffer 中.
System.err.println(intBuffer.get()); // 讀取到0
System.err.println(intBuffer.get()); // 讀取到0

Buffer 的比較

咱們能夠經過 equals() 或 compareTo() 方法比較兩個 Buffer, 當且僅當以下條件知足時, 兩個 Buffer 是相等的:

  • 兩個 Buffer 是相同類型的

  • 兩個 Buffer 的剩餘的數據個數是相同的

  • 兩個 Buffer 的剩餘的數據都是相同的.

經過上述條件咱們能夠發現, 比較兩個 Buffer 時, 並非 Buffer 中的每一個元素都進行比較, 而是比較 Buffer 中剩餘的元素.

本文由 yongshun 發表於我的博客, 採用署名-非商業性使用-相同方式共享 3.0 中國大陸許可協議.非商業轉載請註明做者及出處. 商業轉載請聯繫做者本人Email: yongshun1228@gmail.com本文標題爲: Java NIO 的前生今世 之三 NIO Buffer 詳解本文連接爲: segmentfault.com/a/1190000006824155

相關文章
相關標籤/搜索