1. 程式人生 > >Java的NIO之ByteBuffer底層分析

Java的NIO之ByteBuffer底層分析

http://kakajw.iteye.com/blog/1797073

類ByteBuffer是Java nio程式經常會用到的類,也是重要類 ,我們通過原始碼分析該類的實現原理。


一.ByteBuffer類的繼承結構

public abstract class ByteBuffer
extends Buffer
implements Comparable<ByteBuffer>

ByteBuffer的核心特性來自Buffer


二. ByteBuffer和Buffer的核心特性
A container for data of a specific primitive type. 用於特定基本型別資料的容器。
子類ByteBuffer支援除boolean型別以外的全部基本資料型別。


補充,回顧Java的基本資料型別

Java語言提供了八種基本型別,六種數字型別(四個整數型,兩個浮點型),一種字元型別,一種布林型。

1、整數:包括int,short,byte,long 
2、浮點型:float,double 
3、字元:char 
4、布林:boolean

型別    大小 最小值   最大值
byte    8-bit -128   +127
short  16-bit -2^15   +2^15-1
int       32-bit -2^31   +2^31-1
long    64-bit -2^63   +2^63-1
float    32-bit IEEE754   IEEE754
double 64-bit IEEE754   IEEE754
char    16-bit Unicode 0 Unicode 2^16-1
boolean ----- -----   ------     


本質上,Buffer也就是由裝有特定基本型別資料的一塊記憶體緩衝區和操作資料的4個指標變數(mark標記,position位置, limit界限,capacity容量)組成。不多說,上原始碼:

Java程式碼  收藏程式碼
  1. public abstract class Buffer {  
  2.     // Invariants: mark <= position <= limit <= capacity  
  3.     private int mark = -1;  
  4.     private int position = 0;  
  5.     private int limit;  
  6.     private
     int capacity;  
  7.     ......  
  8. }  
  9. public abstract class ByteBuffer  
  10.     extends Buffer  
  11.     implements Comparable<ByteBuffer>  
  12. {  
  13.     // These fields are declared here rather than in Heap-X-Buffer in order to  
  14.     // reduce the number of virtual method invocations needed to access these  
  15.     // values, which is especially costly when coding small buffers.  
  16.     //  
  17.     final byte[] hb;   // Non-null only for heap buffers  
  18.     final int offset;  
  19.     boolean isReadOnly;   // Valid only for heap buffers  
  20.     ......  
  21. }  

 其中,位元組陣列final byte[] hb就是所指的那塊記憶體緩衝區。


Buffer緩衝區的主要功能特性有:
a.Transferring data  資料傳輸,主要指可通過get()方法和put()方法向緩衝區存取資料,ByteBuffer提供存取除boolean以為的全部基本型別資料的方法。


b.Marking and resetting  做標記和重置,指mark()方法和reset()方法;而標記,無非是儲存操作中某個時刻的索引位置。


c.Invariants 各種指標變數


d.Clearing, flipping, and rewinding 清除資料,位置(position)置0(界限limit為當前位置),位置(position)置0(界限limit不變),指clear()方法, flip()方法和rewind()方法。

e.Read-only buffers 只讀緩衝區,指可將緩衝區設為只讀。

f.Thread safety 關於執行緒安全,指該緩衝區不是執行緒安全的,若多執行緒操作該緩衝區,則應通過同步來控制對該緩衝區的訪問。


g.Invocation chaining 呼叫鏈, 指該類的方法返回呼叫它們的緩衝區,因此,可將方法呼叫組成一個鏈;例如:
 b.flip();
 b.position(23);
 b.limit(42);
等同於
 b.flip().position(23).limit(42);


三.ByteBuffer的結構

ByteBuffer主要由是由裝資料的記憶體緩衝區和操作資料的4個指標變數(mark標記,position位置, limit界限,capacity容量)組成。
記憶體緩衝區:位元組陣列final byte[] hb;
ByteBuffer的主要功能也是由這兩部分配合實現的,如put()方法,就是向陣列byte[] hb存放資料。

Java程式碼  收藏程式碼
  1. ByteBuffer bb = ByteBuffer.allocate(10);   
  2. // 向bb裝入byte資料  
  3. bb.put((byte)9);  

 
底層原始碼的實現如下

Java程式碼  收藏程式碼
  1. class HeapByteBuffer  
  2.     extends ByteBuffer  
  3. {  
  4.     ......  
  5.     public ByteBuffer put(byte x) {  
  6.       hb[ix(nextPutIndex())] = x;  
  7.       return this;  
  8.     }  
  9.     ......  
  10.     final int nextPutIndex() {      
  11.       if (position >= limit)  
  12.       throw new BufferOverflowException();  
  13.        return position++;  
  14.     }  
  15.     ......  
  16. }  

如上所述,bb.put((byte)9);執行時,先判斷position 是否超過 limit,否則指標position向前移一位,將位元組(byte)9存入position所指byte[] hb索引位置。

get()方法相似;

Java程式碼  收藏程式碼
  1. public byte get() {  
  2.    return hb[ix(nextGetIndex())];  
  3. }  

4個指標的涵義

position:位置指標。微觀上,指向底層位元組陣列byte[] hb的某個索引位置;巨集觀上,是ByteBuffer的操作位置,如get()完成後,position指向當前(取出)元素的下一位,put()方法執行完成後,position指向當前(存入)元素的下一位;它是核心位置指標。

mark標記:儲存某個時刻的position指標的值,通過呼叫mark()實現;當mark被置為負值時,表示廢棄標記。

capacity容量:表示ByteBuffer的總長度/總容量,也即底層位元組陣列byte[] hb的容量,一般不可變,用於讀取。

limit界限:也是位置指標,表示待操作資料的界限,它總是和讀取或存入操作相關聯,limit指標可以被  改變,可以認為limit<=capacity。

  ByteBuffer結構如下圖所示



 

四. ByteBuffer的關鍵方法實現

  1.取元素

Java程式碼  收藏程式碼
  1. public abstract byte get();  
  2. //HeapByteBuffer子類實現  
  3. public byte get() {  
  4.    return hb[ix(nextGetIndex())];  
  5. }  
  6. //HeapByteBuffer子類方法  
  7. final int nextGetIndex() {                
  8.     if (position >= limit)  
  9.  throw new BufferUnderflowException();  
  10.    return position++;  
  11. }   

 2.存元素

Java程式碼  收藏程式碼
  1. public abstract ByteBuffer put(byte b);  
  2.  //HeapByteBuffer子類實現  
  3.  public ByteBuffer put(byte x) {  
  4.      hb[ix(nextPutIndex())] = x;  
  5.      return this;  
  6.  }  

    
  3.清除資料   

Java程式碼  收藏程式碼
  1. public final Buffer clear() {  
  2.     position = 0;  
  3.     limit = capacity;  
  4.     mark = -1;  
  5.     return this;  
  6. }  

     
    可見,對於clear()方法,ByteBuffer只是重置position指標和limit指標,廢棄mark標記,並沒有真正清空緩衝區/底層位元組陣列byte[] hb的資料;
    ByteBuffer也沒有提供真正清空緩衝區資料的介面,資料總是被覆蓋而不是清空。
    例如,對於Socket讀操作,若從socket中read到資料後,需要從頭開始存放到緩衝區,而不是從上次的位置開始繼續/連續存放,則需要clear(),重置position指標,但此時需要注意,若read到的資料沒有填滿緩衝區,則socket的read完成後,不能使用array()方法取出緩衝區的資料,因為array()返回的是整個緩衝區的資料,而不是上次read到的資料。


  4. 以位元組陣列形式返回整個緩衝區的資料/byte[] hb的資料

Java程式碼  收藏程式碼
  1. public final byte[] array() {  
  2.     if (hb == null)  
  3.  throw new UnsupportedOperationException();  
  4.      if (isReadOnly)  
  5.         throw new ReadOnlyBufferException();  
  6.      return hb;  
  7. }  

  5.flip-位置重置

Java程式碼  收藏程式碼
  1. public final Buffer flip() {  
  2.     limit = position;  
  3.     position = 0;  
  4.     mark = -1;  
  5.     return this;  
  6. }  

    socket的read操作完成後,若需要write剛才read到的資料,則需要在write執行前執行flip(),以重置操作位置指標,儲存操作資料的界限,保證write資料準確。   
    
 6.rewind-位置重置

Java程式碼  收藏程式碼
  1. public final Buffer rewind() {  
  2.      position = 0;  
  3.      mark = -1;  
  4.      return this;  
  5. }  

   Rewinds this buffer. The position is set to zero and the mark is discarded.
  和flip()相比較而言,沒有執行limit = position;

7.判斷剩餘的操作資料或者剩餘的操作空間

Java程式碼  收藏程式碼
  1. public final int remaining() {  
  2.     return limit - position;  
  3. }  

   常用於判斷socket的write操作中未寫出的資料;

 8.標記

Java程式碼  收藏程式碼
  1. public final Buffer mark() {  
  2.     mark = position;  
  3.     return this;  
  4. }  

   
  9.重置到標記

Java程式碼  收藏程式碼
  1. public final Buffer reset() {  
  2.     int m = mark;  
  3.     if (m < 0)  
  4.       throw new InvalidMarkException();  
  5.     position = m;  
  6.     return this;  
  7. }  

 
五.建立ByteBuffer物件的方式

   1.allocate方式

Java程式碼  收藏程式碼
  1. public static ByteBuffer allocate(int capacity) {  
  2.   if (capacity < 0)  
  3.       throw new IllegalArgumentException();  
  4.       return new HeapByteBuffer(capacity, capacity);  
  5. }  
  6. HeapByteBuffer(int cap, int lim) {  // package-private  
  7.      super(-10, lim, cap, new byte[cap], 0);  
  8.      /* 
  9.      hb = new byte[cap]; 
  10.      offset = 0; 
  11.      */  
  12. }  
  13. // Creates a new buffer with the given mark, position, limit, capacity,  
  14. // backing array, and array offset  
  15. //  
  16. ByteBuffer(int mark, int pos, int lim, int cap, // package-private  
  17.         byte[] hb, int offset)  
  18. {  
  19.   super(mark, pos, lim, cap);  
  20.   this.hb = hb;  
  21.   this.offset = offset;  
  22. }  
  23. // Creates a new buffer with the given mark, position, limit, and capacity,  
  24. // after checking invariants.  
  25. //  
  26. Buffer(int mark, int pos, int lim, int cap) { // package-private  
  27.     if (cap < 0)  
  28.       throw new IllegalArgumentException();  
  29.      this.capacity = cap;  
  30.      limit(lim);  
  31.      position(pos);  
  32.      if (mark >= 0) {  
  33.        if (mark > pos)  
  34.           throw new IllegalArgumentException();  
  35.        this.mark = mark;  
  36.      }  
  37.  }  
  38. p;  

    由此可見,allocate方式建立ByteBuffer物件的主要工作包括: 新建底層位元組陣列byte[] hb(長度為capacity),mark置為-1,position置為0,limit置為capacity,capacity為使用者指定的長度。

   2.wrap方式

Java程式碼  收藏程式碼
  1. public static ByteBuffer wrap(byte[] array) {  
  2. return wrap(array, 0, array.length);  
  3.    }  
  4.    public static ByteBuffer wrap(byte[] array,  
  5.                 int offset, int length)  
  6.    {  
  7.        try {  
  8.           return new HeapByteBuffer(array, offset, length);  
  9.        } catch (IllegalArgumentException x) {  
  10.           throw new IndexOutOfBoundsException();  
  11.        }  
  12.    }  
  13.    HeapByteBuffer(byte[] buf, int off, int len) { // package-private  
  14.         super(-1, off, off + len, buf.length, buf, 0);  
  15.        /* 
  16.        hb = buf; 
  17.        offset = 0; 
  18.        */  
  19.     }  

   wrap方式和allocate方式本質相同,不過因為由使用者指定的引數不同,引數為byte[] array,所以不需要新建位元組陣列,byte[] hb置為byte[] array,mark置為-1,position置為0,limit置為array.length,capacity置為array.length。

       六、結論

        由此可見,ByteBuffer的底層結構清晰,不復雜,原始碼仍是弄清原理的最佳文件。
讀完此文,應該當Java nio的SocketChannel進行read或者write操作時,ByteBuffer的四個指標如何移動有了清晰的認識。

Java NIO學習筆記之二-圖解ByteBuffer

概述

ByteBuffer是NIO裡用得最多的Buffer,它包含兩個實現方式:HeapByteBuffer是基於Java堆的實現,而DirectByteBuffer則使用了unsafe的API進行了堆外的實現。這裡只說HeapByteBuffer。

使用

ByteBuffer最核心的方法是put(byte)get()。分別是往ByteBuffer裡寫一個位元組,和讀一個位元組。

值得注意的是,ByteBuffer的讀寫模式是分開的,正常的應用場景是:往ByteBuffer裡寫一些資料,然後flip(),然後再讀出來。

這裡插兩個Channel方面的物件,以便更好的理解Buffer。

ReadableByteChannel是一個從Channel中讀取資料,並儲存到ByteBuffer的介面,它包含一個方法:

<!-- lang: java -->
public int read(ByteBuffer dst) throws IOException;

WritableByteChannel則是從ByteBuffer中讀取資料,並輸出到Channel的介面:

<!-- lang: java -->
public int write(ByteBuffer src) throws IOException;

那麼,一個ByteBuffer的使用過程是這樣的:

<!-- lang: java -->
byteBuffer = ByteBuffer.allocate(N);
//讀取資料,寫入byteBuffer
readableByteChannel.read(byteBuffer);
//變讀為寫
byteBuffer.flip();
//讀取byteBuffer,寫入資料
writableByteChannel.write(byteBuffer);

看到這裡,一般都不太明白flip()幹了什麼事,先從ByteBuffer結構說起:

ByteBuffer內部欄位

byte[] buff

buff即內部用於快取的陣列。

position

當前讀取的位置。

mark

為某一讀過的位置做標記,便於某些時候回退到該位置。

capacity

初始化時候的容量。

limit

讀寫的上限,limit<=capacity。

圖解

put

寫模式下,往buffer裡寫一個位元組,並把postion移動一位。寫模式下,一般limit與capacity相等。 bytebuffer-put

flip

寫完資料,需要開始讀的時候,將postion復位到0,並將limit設為當前postion。 bytebuffer-flip

get

從buffer裡讀一個位元組,並把postion移動一位。上限是limit,即寫入資料的最後位置。 bytebuffer-get

clear

將position置為0,並不清除buffer內容。 bytebuffer-clear

mark相關的方法主要是mark()(標記)和reset()(回到標記),比較簡單,就不畫圖了。