【NIO】Java NIO之緩衝

leesf發表於2017-04-15

一、前言

  在筆者打算學習Netty框架時,發現很有必要先學習NIO,因此便有了本博文,首先介紹的是NIO中的緩衝。

二、緩衝

  2.1 層次結構圖

  除了布林型別外,其他基本型別都有相對應的緩衝區類,其繼承關係層次圖如下。

  其中,Buffer是所有類的父類,Buffer中也規定了所有緩衝區的共同行為。

  2.2 緩衝區基礎

  緩衝區是包在一個物件內的基本資料元素陣列,其有四個重要屬性

    容量( Capacity):緩衝區能夠容納的資料元素的最大數量,容量在緩衝區建立時被設定,並且永遠不能被改變。

    上界(Limit):緩衝區的第一個不能被讀或寫的元素。或者說,緩衝區中現存元素的計數。

    位置(Position):下一個要被讀或寫的元素的索引。位置會自動由相應的 get( )put( )函式更新。     

    標記(Mark):一個備忘位置。呼叫 mark( )來設定 mark = postion。呼叫 reset( )設定 position = mark。標記在設定前是未定義的(undefined)。

  四個屬性之間的關係如下

    0 <= mark <= position <= limit <= capacity

  如下圖展示了一個容量為10的ByteBuffer邏輯檢視。

  

  其中,mark未被設定,position初始為0,capacity為10,limit為10,第一個元素存放至position為0的位置,capacity不變,其他三個屬性會變化。position在呼叫 put()時,會自動指出了下一個資料元素應該被插入的位置,或者當 get()被呼叫時指出下一個元素應從何處取出。

  當put完資料後需要讀取時,需要呼叫flip函式,其將limit設定為position,然後將position設定為0, 之後開始讀取。

  當對緩衝區呼叫兩次flip函式時,緩衝區的大小變為0。因為第二次的limit也設定為0,position為0,因此緩衝區大小為0。

  緩衝區並不是多執行緒安全的。如果想以多執行緒同時存取特定的緩衝區,則需要在存取緩衝區之前進行同步。

  下面是一個簡單的緩衝區讀寫的示例 

import java.nio.CharBuffer;


/**
 * Created by LEESF on 2017/4/15.
 */
public class BufferDemo {
    public static void main(String[] args) {
        CharBuffer buffer = CharBuffer.allocate(5);
        buffer.put('H');
        buffer.put('E');
        buffer.put('L');
        buffer.put('L');
        buffer.put('O');

        buffer.flip();
        while (buffer.hasRemaining()) {
            System.out.print(buffer.get());
        }
    }
}

  執行結果如下  

HELLO

  當對緩衝區進行比較時,判定兩個緩衝區相同充要條件如下

    · 兩個物件型別相同。包含不同資料型別的 buffer 永遠不會相等,而且 buffer絕不會等於非 buffer 物件。

    · 兩個物件都剩餘同樣數量的元素。Buffer 的容量不需要相同,而且緩衝區中剩餘資料的索引也不必相同。但每個緩衝區中剩餘元素的數目(從位置到上界)必須相同。

    · 在每個緩衝區中應被 get()函式返回的剩餘資料元素序列必須一致。

  如果不滿足以上任意條件, 兩個緩衝區的比較就會返回 false。

  當兩個緩衝區不一樣長,進行比較時,如果一個緩衝區在不相等元素髮現前已經被耗盡,較短的緩衝區被認為是小於較長的緩衝區。

  當緩衝區與陣列進行互動時,如果緩衝區中的資料不夠完全填滿陣列,就會得到一個異常。這意味著如果想將一個小型緩衝區傳入一個大型陣列,就必須明確地指定緩衝區中剩餘的資料長度。如果緩衝區有足夠的空間接受陣列中的資料( buffer.remaining()>myArray.length),資料將會被複制到從當前位置開始的緩衝區,並且緩衝區位置會被提前所增加資料元素的數量。如果緩衝區中沒有足夠的空間,那麼不會有資料被傳遞,同時丟擲BufferOverflowException 異常。

  2.3 建立緩衝區

  新的緩衝區由分配(allocate)或包裝(wrap)操作建立的。分配操作建立一個緩衝區物件並分配一個私有的空間來儲存指定容量大小的資料。包裝操作建立一個緩衝區物件但是不分配任何空間來儲存資料元素,使用所提供的陣列作為儲存空間來儲存緩衝區中的資料。

  使用分配方式建立緩衝區如下 ,如建立容量大小為100的CharBuffer。  

CharBuffer charBuffer = CharBuffer.allocate (100);

  使用包裝方式建立緩衝區如下 

char [] myArray = new char [100];
CharBuffer charbuffer = CharBuffer.wrap (myArray);

  上述程式碼構造了一個新的緩衝區物件,但資料元素會存在於陣列中。這意味著通過呼叫put()函式造成的對緩衝區的改動會直接影響這個陣列,而且對這個陣列的任何改動也會對這個緩衝區物件可見。 

  帶有 offset 和 length 作為引數的 wrap()函式版本則會構造一個按照指定的 offset 和 length 引數值初始化位置和上界的緩衝區。 

CharBuffer charbuffer = CharBuffer.wrap (myArray, 12, 42);

  上述程式碼建立了一個 position 值為 12, limit 值為 54(12 + 42),容量為 myArray.length 的緩衝區。

   2.4 複製緩衝區

  當一個管理其他緩衝器所包含的資料元素的緩衝器被建立時,這個緩衝器被稱為檢視緩衝器,而檢視緩衝器總是通過呼叫已存在的儲存器例項中的函式來建立。  

  如Duplicate()函式建立了一個與原始緩衝區相似的新的緩衝區,兩個緩衝區共享資料元素,擁有同樣的容量,但每個緩衝區擁有各自的位置,上界和標記屬性。對一個緩衝區內的資料元素所做的改變會反映在另外一個緩衝區上。這一副本緩衝區具有與原始緩衝區同樣的資料檢視。如果原始的緩衝區為只讀,或者為直接緩衝區,新的緩衝區將繼承這些屬性。即複製一個緩衝區會建立一個新的 Buffer 物件,但並不複製資料,原始緩衝區和副本都會操作同樣的資料元素

  如下面程式碼片段  

CharBuffer buffer = CharBuffer.allocate (8);
buffer.position (3).limit (6).mark( ).position (5);
CharBuffer dupeBuffer = buffer.duplicate( );
buffer.clear( );

  會建立一個如下圖所示的緩衝檢視

  

  可以看到,複製的緩衝繼承了四個屬性值,操作的底層都是同一份資料,每個檢視對資料的操作都會反映到另外一個檢視上,如下述程式碼可驗證。

import java.nio.CharBuffer;

/**
 * Created by LEESF on 2017/4/13.
 */
public class AllocateDemo {
    public static void main(String[] args) {
        CharBuffer buffer = CharBuffer.allocate (8);
        buffer.put('L');
        buffer.put('E');
        buffer.put('E');
        buffer.put('S');
        buffer.put('F');
        buffer.position (3).limit (6).mark( ).position (5);
        CharBuffer dupeBuffer = buffer.duplicate( );
        buffer.clear( );
        dupeBuffer.flip();
        System.out.println(dupeBuffer.position());
        System.out.println(dupeBuffer.limit());
        System.out.println(dupeBuffer.get());

        buffer.put('Y');
        buffer.put('D');
        buffer.flip();
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.get());

        System.out.println(dupeBuffer.get());
    }
}

  執行結果如下 

0
5
L
0
2
Y
D

  可以看到buffer檢視對資料的寫入影響了dupeBuffer的資料獲取。

  2.5 位元組緩衝區

  位元組是作業系統及其 I/O 裝置使用的基本資料型別。當在 JVM 和作業系統間傳遞資料時,也是使用欄位進行傳遞。

  每個基本資料型別都是以連續位元組序列的形式儲存在記憶體中,如32 位的 int 值0x037fb4c7(十進位制的 58,700,999)可能會如下圖所示的被儲存記憶體位元組中(記憶體地址從左往右增加)。

  

  多位元組數值被儲存在記憶體中的方式一般被稱為 endian-ness(位元組順序),如果數字數值的最高位元組——big end(大端),位於低位地址,那麼系統就是大端位元組順序,如果最低位元組最先儲存在記憶體中,那麼小端位元組順序,如下圖所示。

  

  預設位元組順序總是 ByteBuffer.BIG_ENDIAN,無論系統的固有位元組順序是什麼。Java 的預設位元組順序是大端位元組順序

  為了解決非直接緩衝區(如通過wrap()函式所建立的被包裝的緩衝區)的效率問題,引入了直接緩衝區,直接緩衝區使用的記憶體是通過呼叫本地作業系統方面的程式碼分配的,繞過了標準 JVM 堆疊,因此效率更高。ByteBuffer中存在isDirect方法判斷緩衝區是否是直接緩衝區。

  位元組緩衝區可以轉化為其他不同型別的緩衝區,如CharBuffer、ShortBuffer等,轉化後的緩衝區都只是原來緩衝區的檢視,即有獨立的四個屬性值,但是共享資料元素。

  位元組緩衝區可以直接存放或者取出不同型別的資料元素,如直接put(Char char)、put(int value)等。當put時,其會將不同型別資料轉化為位元組型別從position位置開始依次存放,而當get時,其會根據不同的get型別,從position位置開始依次取出對應的位元組數轉化後返回。

三、總結

  本篇博文講解了NIO中的緩衝區,最核心的就是四個屬性,所有針對緩衝區的操作都是基於四個屬性的操作,讀者想要更具體的瞭解緩衝區的內容,可以查閱原始碼,謝謝各位園友的觀看~

相關文章