NIO 一 緩衝區

一 緩衝區

1.1 什麼是緩衝區

  緩衝區就是一塊指定大小的內存空間,緩衝區存在的意義其一是減少實際物理讀寫次數,將IO設備和CPU進行隔離,使得CPU快速讀寫緩衝區後可執行其他任務,IO設備面向緩衝區而無需長時間佔用CPU;其二在於減少內存碎片,系統運行時即分配固定大小的緩衝區,並且讀寫複用,減少了內存空間不連續觸發的動態分配和內存回收次數。

1.2 直接緩衝區

  直接緩衝區是一個和JVM相關的概念,它是對緩衝區的一個分類,分類的維度在於數據交互模式不同。

  如果緩衝區進行數據讀寫時,JVM創建一箇中間緩衝區暫存數據,然後再傳遞給緩衝區,這類緩衝區非直接緩衝區。相對的,如果數據交互直接在內核空間中處理,而不經過中間緩衝區,這類緩衝區就是直接緩衝區。緩衝區是否爲直接緩衝區,和創建緩衝區的API有關。

  提前介紹這一點非常重要,後文中對部分API的介紹會反覆提及此概念。

1.3 NIO緩衝區實現

  Java NIO中將緩衝區抽象爲Buffer類型,Buffer本身是一個抽象類。從Buffer派生出了7個抽象子類:

  1. ByteBuffer
  2. CharBuffer
  3. DoubleBuffer
  4. FloatBuffer
  5. IntBuffer
  6. LongBuffer
  7. ShortBuffer。

  需要注意的是沒有StringBuffer,因爲StringBuffer是lang包下的,上述7個抽象子類是nio包下的。另一個需要注意的是,NIO中沒有boolean類型的Buffer,因爲boolean作爲緩衝數據毫無意義。

  之所以將子類設計爲抽象類,是因爲創建Buffer時的具體類型依調用的API決定。如ByteBuufer.wrap方法會返回一個HeapByteBuffer類型的對象,而通過ByteBuffer.allocateDirect方法創建的緩衝區是DirectByteBuffer直接緩衝區類型。

  派生出諸多類型的子類的原因在於傳統的和IO相關的API,都是依賴byte[]和char[]實現的,而Java中對數組的進行操作的API非常少,基本上只有length屬性和通過索引下標進行訪問和設置的方法,爲了方便開發者使用基於數組實現的過程複雜的數據讀寫操作,這才引入了Buffer設計。

  嚴格意義上講緩衝區就是一塊內存區域,區別於其他內存區域如堆、棧等,主要在於其應用場景不同:大小確定,暫存數據,讀寫複用。滿足這三個應用需求時,才考慮使用Buffer。

二 核心參數

2.1 參數介紹

  NIO中的緩衝區提供的所有API都針對數據讀寫,存儲的數據則通過四個核心參數控制:

參數名 含義 介紹
capacity 容量 緩衝區尺寸,不能爲負,無法修改,創建緩衝區時確定
limit 限制 緩衝區可讀寫的數據長度,不能爲負,不能大於capacity
position 位置 當前可讀寫位置,不能大於limit
mark 標記 用於復位position位置,配合reset方法使用,不能大於position

2.2 參數限定條件

  上述參數的數值必須滿足如下限定條件:

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

  如果核心參數值不滿足限定條件,程序運行中會拋出對應的異常,如設置和訪問超過limit限制的數據時拋出java.lang.IndexOutOfBoundsExceptio:

public class CharBufferTest {
    public static void main(String[] args) throws Exception {
        char[] chars = new char[]{'a', 'b', 'c'};
        CharBuffer charBuffer = CharBuffer.wrap(chars);
        charBuffer.limit(3);
		// charBuffer.set(3);
        charBuffer.get(3);
    }
}

  運行結果如下:

Exception in thread "main" java.lang.IndexOutOfBoundsException
	at java.nio.Buffer.checkIndex(Buffer.java:540)
	at java.nio.HeapCharBuffer.get(HeapCharBuffer.java:139)
	at com.eframesoft.nio.CharBufferTest.main(CharBufferTest.java:12)

2.3 參數終值

  需要注意的是,設置參數時會依照既定的規則確定參數最終的值,這一點非常重要,很多時候如果讀者發現進行緩衝區參數設置時,莫名的出現了一些難以解釋的問題,那麼需要根據下述場景進行分析(僅列出我已知的部分,更多場景後續補充,或者請讀者自行查閱DOC或源碼):

場景 規則
設置limit值時,如果pisition > limit position被設置爲limit值
設置limit/position值時,如果mark > limit/position 丟棄mark,mark=-1
limit = position 寫入數據異常,讀取數據爲空

2.4 參數操作API

  Buffer作爲緩衝區抽象基類,針對上述4個核心參數提供了訪問/設置(部分參數允許設置)和相關計算的API,可以說整個緩衝區API設計都是圍繞核心參數來實現的,基類Buffer已經提供了非常詳細的API實現,派生類可視情況重寫,下面針對參數值的設置和訪問API進行介紹。

  因篇幅受限,本章節僅列出圍繞核心參數操作的常用API,其他API說明請參考DOC或查閱源碼。另請讀者注意,很多參數設置方法會返回此Buffer對象引用,這樣設計僅僅是爲了可以級聯調用,無需對返回值太過關注。

2.4.1 獲取緩衝區尺寸

  int capacity(),返回緩衝區的實際大小,示例如下:

CharBuffer charBuffer = CharBuffer.wrap(new char[]{'a', 'b', 'c'});
System.out.println(charBuffer.capacity());

輸出結果;
3

2.4.2 獲取緩衝區可讀寫尺寸限制

  int limit(),返回緩衝區的限制大小,示例如下:

CharBuffer charBuffer = CharBuffer.wrap(new char[]{'a', 'b', 'c'});
System.out.println(charBuffer.limit());

輸出結果:
3

2.4.3 設置緩衝區可讀寫尺寸限制

   Buffer limit(int newLimit), 設置緩衝區的限制大小,嚴格意義上講,limit表示第一個不允許讀寫的數據的索引值,示例如下:

CharBuffer charBuffer = CharBuffer.wrap(new char[]{'a', 'b', 'c'});
System.out.println(charBuffer.limit(2));
System.out.println(charBuffer.limit());

輸出結果:
ab
2

2.4.4 獲取緩衝區可讀寫數據索引

   int position(),返回此緩衝區下一個可讀寫的數據的索引值,示例如下:

CharBuffer charBuffer = CharBuffer.wrap(new char[]{'a', 'b', 'c'});
System.out.println(charBuffer.position());

輸出結果:
0

2.4.5 設置緩衝區可讀寫位置索引

  Buffer position(int newPosition),設置此緩衝區下一個可讀寫的數據的索引值,示例如下:

CharBuffer charBuffer = CharBuffer.wrap(new char[]{'a', 'b', 'c'});
System.out.println(charBuffer.position(1));
System.out.println(charBuffer.position());

輸出結果:
bc
1

  需要注意的是:

  1. 如果position等於limit值,那麼後續讀數據的時候是讀不到的,但是不會報錯;
  2. 如果position大於limit值,那麼會拋出IllegalArgumentException異常。

  示例如下:

CharBuffer charBuffer = CharBuffer.wrap(new char[]{'a', 'b', 'c'});
for (int i = 0; i < charBuffer.capacity(); i++) {
	System.out.println("設置下一個可讀寫數據索引值:" + charBuffer.position(i) + " 下一個可讀寫數據的索引值:" + charBuffer.position());
}

charBuffer.position(3);
System.out.println(charBuffer.position());

charBuffer.position(4);
System.out.println(charBuffer.position());

輸出結果:
設置下一個可讀寫數據索引值:abc 下一個可讀寫數據的索引值:0
設置下一個可讀寫數據索引值:bc 下一個可讀寫數據的索引值:1
設置下一個可讀寫數據索引值:c 下一個可讀寫數據的索引值:2
3
Exception in thread "main" java.lang.IllegalArgumentException
	at java.nio.Buffer.position(Buffer.java:244)
	at com.eframesoft.nio.CharBufferTest.main(CharBufferTest.java:15)

2.4.6 獲取可用緩衝區大小

  int remaining(),返回position和limit參數差值,這部分數據長度就是此緩衝區尚可使用的內存空間大小,示例如下:

CharBuffer charBuffer = CharBuffer.wrap(new char[]{'a', 'b', 'c'});
System.out.println(charBuffer.remaining());

輸出結果:
3

2.4.7 標記重置位置

  Buffer mark(),這個API比較難理解,是因爲它單獨存在毫無意義,配合使用的是另一個API reset(),mark方法可以在小於position值的位置上打上一個標記,當調用reset方法的時候,將position重置爲mark標記值。

  mark值的約束較多,且這並非是一個必要的屬性,但如果使用不當會出現很多難以理解的問題,這部分請參考前文參數終值部分。

  需要注意的是設置limit或者position時,limit/position的值小於當前mark值,那麼mark會被強制設置爲-1,此時如果調用reset方法重置position,會拋出InvalidMarkException異常,示例如下:

CharBuffer charBuffer = CharBuffer.wrap(new char[]{'a', 'b', 'c'});
// 在position2處標記
charBuffer.position(2);
charBuffer.mark();
System.out.println("當前位置:" + charBuffer.position());
// 用get訪問下一個數據,position自增1
charBuffer.get();
System.out.println("當前位置:" + charBuffer.position());
// 重置position
charBuffer.reset();
System.out.println("當前位置:" + charBuffer.position());
// 重設限制值,使mark丟棄,值爲-1
charBuffer.limit(1);
charBuffer.reset();

輸出結果:
當前位置:2
當前位置:3
當前位置:2
Exception in thread "main" java.nio.InvalidMarkException
	at java.nio.Buffer.reset(Buffer.java:306)
	at com.eframesoft.nio.CharBufferTest.main(CharBufferTest.java:20)

2.4.8 重置核心參數

  final Buffer clear(),將緩衝區的核心參數還原爲初始狀態(因緩衝區大小capacity無法更改,所以不作調整,limit設置爲capacity,position設置爲0,mark設置爲-1),這是Buffer中非常核心的方法,常用於複用此緩衝區進行數據寫入前,爲了保證核心參數的語義一致,所以此方法被聲明爲final,不允許派生類重寫:

CharBuffer charBuffer = CharBuffer.wrap(new char[]{'a', 'b', 'c'});
charBuffer.limit(2);
charBuffer.position(1);
System.out.println("capacity:" + charBuffer.capacity() + " limit:" + charBuffer.limit() + " position:" + charBuffer.position());
charBuffer.clear();
System.out.println("capacity:" + charBuffer.capacity() + " limit:" + charBuffer.limit() + " position:" + charBuffer.position());

輸出結果:
capacity:3 limit:2 position:1
capacity:3 limit:3 position:0

  另外讀者必須要注意,clear方法僅僅將核心參數重置,緩衝區中的數據是不會丟失的,只不過是通過寫入新的數據來進行數據內容的覆蓋而已,這也是緩衝區支持複用的基礎,如果讀寫數據時核心參數設置不正確,那麼是極有可能出現讀寫錯誤數據的!!!與之類似的還有下面小節中介紹的方法。

2.4.9 寫讀模式轉換

  final Buffer flip(),按API文檔中描述此方法用作反轉此緩衝區,然而我還是喜歡將其稱作寫讀模式轉換,原因無他,此方法常用於向緩衝區中寫入數據後,讀寫入數據前。

  這個方法的聲明依然是final的,其內部實現的邏輯就是將limit設置爲position,再將position設置爲0,mark直接丟棄。在寫入數據後調用此方法,即可保證從position=0位置開始讀數據,且讀出的數據位置不會超過當初寫入的數據長度(因爲將limit設置爲寫入是的position了),因此我把這個方法稱作寫讀模式轉換,示例如下:

CharBuffer charBuffer = CharBuffer.wrap(new char[]{'a', 'b', 'c'});
charBuffer.put('1');
charBuffer.put('2');
System.out.println("capacity:" + charBuffer.capacity() + " limit:" + charBuffer.limit() + " position:" + charBuffer.position());
charBuffer.flip();
for (int i = 0; i < charBuffer.limit(); i++) {
	System.out.println(charBuffer.get(i));
}

輸出結果:
capacity:3 limit:3 position:2
1
2

2.4.10 數據重讀

  final Buffer rewind(),這名字還算形象,實現也很簡單,將position設置爲0,mark設置爲-1,limit不變,這樣實現的目的是爲了方便重新讀取緩衝數據,示例如下:

CharBuffer charBuffer = CharBuffer.wrap(new char[]{'a', 'b', 'c'});
for (int i = charBuffer.position(); i < charBuffer.limit(); i++) {
	System.out.println(charBuffer.get() + " position:" + charBuffer.position());
}
charBuffer.rewind();
System.out.println(charBuffer.position());

輸出結果:
a position:1
b position:2
c position:3
0

三 緩衝區API

  章節2.4 參數操作API中介紹了操作緩衝區核心參數相關的API,除此之外,基類Buffer還提供了許多緩衝區相關的方法,這些方法未必會在程序設計中大量使用,但是對NIO和一些關鍵設計的理解非常有幫助。

3.1 緩衝區是否只讀

  boolean isReadOnly(),判斷此緩衝區數據是否只讀,這是一個抽象方法,由具體的緩衝區類型提供是否只讀信息,說實話我不是很喜歡這個方法命名,因爲所有的緩衝區都是可讀的,差別僅在於某些緩衝數據是不可寫的,如緩衝一個只讀文件時,緩衝數據是不可寫的狀態。

 CharBuffer charBuffer = CharBuffer.wrap(new char[]{'a', 'b', 'c'});
System.out.println(charBuffer.isReadOnly());

輸出結果:
false

  當然我們可以通過一個可讀寫的緩衝區生成一個只讀的緩衝區,通過asReadOnlyBuffer方法實現:

CharBuffer charBuffer = CharBuffer.wrap(new char[]{'a', 'b', 'c'});
System.out.println(charBuffer.isReadOnly());
CharBuffer newCharBuffer = charBuffer.asReadOnlyBuffer();
System.out.println(newCharBuffer.isReadOnly());

輸出結果:
false
true

3.2 是否爲直接緩衝區

  章節1.2 直接緩衝區中已經介紹過了什麼是直接緩衝區,那麼程序中通過boolean isDirect()方法來進行判定。基本上靜態函數wraphe allocate創建的緩衝區都是非直接緩衝區,而通過allocateDirect方法創建的緩衝區是直接緩衝區,但是一定要注意因爲直接緩衝區數據交互不在JVM中,那麼數據類型就必須是系統能接受的類型,可用類型爲ByteBuffer,其他Buffer的直接派生類中沒有此方法:

 CharBuffer charBuffer = CharBuffer.wrap(new char[]{'a', 'b', 'c'});
System.out.println(charBuffer.isDirect());
charBuffer = CharBuffer.allocate(4);
System.out.println(charBuffer.isDirect());
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(10);
System.out.println(byteBuffer.isDirect());

輸出結果:
false
false
true

3.3 是否由數組實現

  前文中介紹過,緩衝區實際上就是一塊內存區,且Buffer的直接派生類僅僅是針對不同的數據類型來將其存儲輸出的數組包裝爲緩衝區類型,那麼這個數組是否創建在JVM內存中呢?通過方法final boolean hasArray()進行判定。包含中間緩衝區的一定是由JVM數組實現的,而直接緩衝區數據直接和內核交互,因此不存在JVM數組:

CharBuffer charBuffer = CharBuffer.wrap(new char[]{'a', 'b', 'c'});
System.out.println(charBuffer.hasArray());
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(10);
System.out.println(byteBuffer.hasArray());

輸出結果:
true
false

四 結語

  其實緩衝區沒那麼複雜,牢牢記住四個核心參數的大小關係,並且按使用場景來理解各個API,緩衝區就很簡單了,比如說重寫數據clear,寫完要讀flip,再讀一遍rewind,等等。

  如果想關注更多硬技能的分享,可以參考積少成多系列傳送門,未來每一篇關於硬技能的分享都會在傳送門中更新鏈接。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章