當前位置:
首頁 > 知識 > JDK 源碼閱讀 :ByteBuffer

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技能


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 ImportNew 的精彩文章:

深入理解單例模式 ( 下 )

TAG:ImportNew |