歡迎光臨
每天分享高質量文章

JDK 原始碼閱讀 :ByteBuffer

(點選上方公眾號,可快速關註)


來源:木杉的部落格 ,

imushan.com/2018/08/07/java/language/JDK原始碼閱讀-ByteBuffer/

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 詳解 – 簡書

    https://www.jianshu.com/p/007052ee3773

【關於投稿】


如果大家有原創好文投稿,請直接給公號傳送留言。


① 留言格式:
【投稿】+《 文章標題》+ 文章連結

② 示例:
【投稿】《不要自稱是程式員,我十多年的 IT 職場總結》:http://blog.jobbole.com/94148/

③ 最後請附上您的個人簡介哈~



看完本文有收穫?請轉發分享給更多人

關註「ImportNew」,提升Java技能

贊(0)

分享創造快樂