酷播亮新聞
最棒的知識補給站

JDK原始碼閱讀:ByteBuffer

文章摘要: 從Buffer維護的position位置開始讀寫當我們需要從位元組緩衝區中讀取別的資料型別才需要使用其他具體型別的Buffer實現

Buffer是Java NIO中對於緩衝區的封裝。在Java BIO中,所有的讀寫API,都是直接使用byte陣列作為緩衝區的,簡單直接。但是在Java NIO中,緩衝區這一概念變得複雜,可能是對應Java堆中的一塊記憶體,也可能是對應本地記憶體中的一塊記憶體。而byte陣列只能用來指定Java堆中的一塊記憶體,所以Java NIO中設計了一個新的緩衝區抽象,涵蓋了不同型別緩衝區,這個抽象就是Buffer。

Buffer

Buffer是Java NIO中對於緩衝區的抽象。是一個用於儲存特定基本資料型別的容器。Buffer是特定基本資料型別的線性有限序列。

Java有8中基本型別:byte,short,int,long,float,double,char,boolean,除了boolean型別外,其他的型別都有對應的Buffer具體實現:

Buffer抽象類定義了所有型別的Buffer都有的屬性和操作,屬性如下:

  • capacity:緩衝區的容量,在緩衝區建立後就不能改變
  • limit:表示第一個不能讀寫的元素位置,limit不會大於capacity
  • position:表示下一個要讀寫的元素位置,position不會大於limit
  • mark:用於暫存一個元素位置,和書籤一樣,用於後續操作

所有的Buffer操作都圍繞這些屬性進行。這些屬性滿足一個不變式:0<=mark<=position<=limit<=capacity

新建的Buffer這些屬性的取值為:

  • position=0
  • limit=capacity=使用者設定的容量
  • mark=-1

直接看定義比較抽象,可以看一下示意圖,下圖是一個容量為10的Buffer:

ByteBuffer的具體實現

所有Buffer實現中,最重要的實現是ByteBuffer,因為作業系統中所有的IO操作都是對位元組的操作。當我們需要從位元組緩衝區中讀取別的資料型別才需要使用其他具體型別的Buffer實現。

ByteBuffer也是一個抽象類,具體的實現有HeapByteBuffer和DirectByteBuffer。分別對應Java堆緩衝區與堆外記憶體緩衝區。Java堆緩衝區本質上就是byte陣列,所以實現會比較簡單。而堆外記憶體涉及到JNI程式碼實現,較為複雜,本次我們以HeapByteBuffer為例來分析Buffer的相關操作,後續專門分析DirectByteBuffer。

ByteBuffer的類圖如下:

讀寫Buffer

Buffer作為緩衝區,最主要的作用是用於傳遞資料。Buffer提供了一系列的讀取與寫入操作。因為不同型別的Buffer讀寫的型別不同,所以具體的方法定義是定義在Buffer實現類中的。與讀寫相關的API如下:

byte get()
byte get(int index)
ByteBuffer get(byte[] dst, int offset, int length)
ByteBuffer get(byte[] dst)

ByteBuffer put(byte b)
ByteBuffer put(int index, byte b)
ByteBuffer put(ByteBuffer src) 
ByteBuffer put(byte[] src, int offset, int length)

Buffer的讀寫操作可以按照兩種維度分類:

  • 單個/批量:
    • 單個:一次讀寫一個位元組
    • 批量:一次讀寫多個位元組
  • 相對/絕對:
    • 相對:從Buffer維護的position位置開始讀寫,讀寫時position會隨之變化
    • 絕對:直接指定讀寫的位置。指定index的API就是絕對API

接著我們來看看這些函式在HeapByteBuffer中是如何實現的:

final byte[] hb;    // 作為緩衝區的byte陣列              
final int offset;   // 指定緩衝區的起始位置

public byte get() {
    // get操作就是直接從陣列中獲取資料
    return hb[ix(nextGetIndex())];
}

public byte get(int i) {
    // 從指定位置獲取資料,是絕對操作,只需檢查下標是否合法
    return hb[ix(checkIndex(i))];
}

// 獲取下一個要讀取的元素的下標
// position的定義就是下一個要讀寫的元素位置,
// 所以這裏是返回position的當前值,然後再對position進行加一操作
final int nextGetIndex() {                          // package-private
    if (position >= limit)
        throw new BufferUnderflowException();
    return position++;
}

// 因為支援偏移量,所以算出來的下標還需要加上偏移量
protected int ix(int i) {
    return i + offset;
}

單位元組put與get邏輯一樣。看一下批量get是如何實現的:

public ByteBuffer get(byte[] dst) {
    return get(dst, 0, dst.length);
}

public ByteBuffer get(byte[] dst, int offset, int length) {
    // 檢查引數是否越界
    checkBounds(offset, length, dst.length);
    // 檢查要獲取的長度是否大於Buffer中剩餘的資料長度
    if (length > remaining())
        throw new BufferUnderflowException();
    // 呼叫System.arraycopy進行陣列內容拷貝
    System.arraycopy(hb, ix(position()), dst, offset, length);
    // 更新position
    position(position() + length);
    return this;
}

可以看出,HeapByteBuffer是封裝了對byte陣列的簡單操作。對緩衝區的寫入和讀取本質上是對陣列的寫入和讀取。使用HeapByteBuffer的好處是我們不用做各種引數校驗,也不需要另外維護陣列當前讀寫位置的變數了。

同時我們可以看到,Buffer中對於position的操作沒有使用鎖進行保護,所以Buffer不是執行緒安全的。

Buffer的模式

雖然JDK的Java Doc並沒有提到Buffer有模式,但是Buffer提供了flip等操作用於切換Buffer的工作模式。在正確使用Buffer時,一定要注意Buffer的當前工作模式。否則會導致資料讀寫不符合你的預期。

Buffer有兩種工作模式,一種是接收資料模式,一種是輸出資料模式。

新建的Buffer處於接收資料的模式,可以向Buffer放入資料,放入一個對應基本型別的資料後,position加一,如果position已經等於limit了還進行put操作,則會丟擲BufferOverflowException異常。

這種模式的Buffer可以用於Channel的read操作緩衝區,或者是用於相對put操作。

比如向一個接受資料模式的Buffer put5個byte後的示例圖:

因為Buffer的設計是讀寫的位置變數都使用position這個變數,所以如果要從Buffer中讀取資料,要切換Buffer到輸出資料模式。Buffer提供了flip方法用於這種切換。

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

切換後的效果圖:

然後就可以從Buffer中讀取資料了。每次讀取一個元素,position就會加一,如果position已經等於limit還進行讀取,會丟擲BufferUnderflowException異常。

可以看出Buffer本身沒有一個用於儲存模式的變數,模式的切換隻是position和limit的變換而已。

flip方法只會把Buffer從接收模式切換到輸出模式,如果要從輸出模式切換到接收模式,可以使用compact或者clear方法,如果資料已經讀取完畢或者資料不要了,使用clear方法,如果已讀的資料需要保留,同時需要切換到接收資料模式,使用compat方法。

// 壓縮Buffer,去掉已經被讀取的資料
// 壓縮後的Buffer處於接收資料模式
public ByteBuffer compact() {
    System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
    position(remaining());
    limit(capacity());
    discardMark();
    return this;
}

// 清空Buffer,去掉所有資料(沒有做清理工作,是指修改位置變數)
// 清空後的Buffer處於接收資料模式
public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}

總結

  • Buffer是Java NIO對緩衝區的抽象
  • 除了boolean型別,其他的基本型別都有對應的Buffer實現
  • 最常用的Buffer實現是ByteBuffer,具體的實現有HeapByteBuffer和DirectByteBuffer,分別對應Java堆緩衝區與對外記憶體緩衝區
  • HeapByteBuffer是對byte陣列的封裝,方便使用
  • Buffer不是執行緒安全的
  • Buffer有兩種模式一種是接收資料模式,一種是輸出資料模式。新建的Buffer處於接收資料模式,使用flip方法可以切換Buffer到輸出資料模式。使用compact或者clear方法可以切換到接收資料模式。

參考資料

  • 堆外記憶體 之 DirectByteBuffer 詳解 – 簡書

如有侵權請來信告知:酷播亮新聞 » JDK原始碼閱讀:ByteBuffer