Java NIO2:緩衝區

五月的倉頡發表於2015-12-27

什麼是緩衝區

一個緩衝區物件是固定數量的資料的容器,其作用是一個儲存器,或者分段運輸區,在這裡資料可被儲存並在之後用於檢索。緩衝區像前篇文章討論的那樣被寫滿和釋放,對於每個非布林原始資料型別都有一個緩衝區類,儘管緩衝區作用於它們儲存的原始資料型別,但緩衝區十分傾向於處理位元組,非位元組緩衝區可以再後臺執行從位元組或到位元組的轉換,這取決於緩衝區是如何建立的。

緩衝區的工作與通道緊密聯絡。通道是I/O傳輸發生時通過的入口,而緩衝區是這些資料傳輸的來源或目標。對於離開緩衝區的傳輸,待傳遞出去的資料被置於一個緩衝區,被傳送到通道;待傳回的緩衝區的傳輸,一個通道將資料放置在所提供的緩衝區中。這種在協同物件之間進行的緩衝區資料傳遞時高效資料處理的關鍵。

 

Buffer類的家譜

下圖是Buffer的類層次圖。在頂部是通用Buffer類,Buffer定義所有緩衝區型別共有的操作,無論是它們所包含的資料型別還是可能具有的特定行為:

 

緩衝區基礎

概念上,緩衝區是包在一個物件內的基本資料元素陣列。Buffer類相比一個簡單陣列的優點是它將關於資料的資料內容和資訊包含在一個單一的物件中,Buffer類以及它專有的子類定義了一個用於處理資料緩衝區的API。下面來看一下Buffer類所具有的屬性和方法:

1、屬性

所有的緩衝區都具有四個屬性來提供關於其所包含的資料元素的資訊,它們是:

屬      性 作      用
capacity 容量,指緩衝區能夠容納的資料元素的最大數量,這一容量在緩衝區建立時被設定,並且永遠不能被改變
limit 上界,指緩衝區的第一個不能被讀或寫的元素,或者說是,緩衝區中現存元素的計數
position 位置,指下一個要被讀或寫的元素的索引,位置會自動由相應的get()和put()函式更新
mark 標記,指一個備忘位置,呼叫mark()來設定mark=position,呼叫reset()來設定postion=mark,標記未設定前是未定義的

這四個屬性總是遵循以下的關係:0 <= mark <= position <= limit <= capacity

2、方法

下面看一下如何使用一個緩衝區,Buffer中提供了以下的一些方法:

方      法 作      用
Object array() 返回此緩衝區的底層實現陣列
int arrayOffset() 返回此緩衝區的底層實現陣列中第一個緩衝區還俗的偏移量
int capacity() 返回此緩衝區的容量
Buffer clear() 清除此緩衝區
Buffer flip() 反轉此緩衝區
boolean hasArray() 告知此緩衝區是否具有可訪問的底層實現陣列
boolean hasRemaining() 告知在當前位置和限制之間是否有元素
boolean isDirect() 告知此緩衝區是否為直接緩衝區
boolean isReadOnly() 告知此緩衝區是否為只讀快取
int limit() 返回此緩衝區的上界
Buffer limit(int newLimit) 設定此緩衝區的上界
Buffer mark() 在此緩衝區的位置設定標記
int position() 返回此緩衝區的位置
Buffer position(int newPosition) 設定此緩衝區的位置
int remaining() 返回當前位置與上界之間的元素數
Buffer reset() 將此緩衝區的位置重置為以前標記的位置
Buffer rewind() 重繞此緩衝區

關於這個API有一點值得注意的,像clear()這類函式,通常應當返回的是void而不是Buffer引用。這些函式將引用返回到它們在(this)上被引用的物件,這是一個允許級聯呼叫的類設計方法。級聯呼叫允許這種型別的程式碼:

buffer.mark();
buffer.position(5);
buffer.reset();

被簡寫成:

buffer.mark().position(5).reset();

 

緩衝區程式碼例項

對緩衝區的使用,先看一段程式碼,然後解釋一下:

 1 public class TestMain
 2 {
 3     /**
 4      * 待顯示的字串
 5      */
 6     private static String[] strs = 
 7     {
 8         "A random string value",
 9         "The product of an infinite number of monkeys",
10         "Hey hey we're the monkees",
11         "Opening act for the Monkees:Jimi Hendrix",
12         "Scuse me while I kiss this fly",
13         "Help Me! Help Me!"
14     };
15     
16     /**
17      * 標識strs的下標索引
18      */
19     private static int index = 0;
20     
21     /**
22      * 向Buffer內放置資料
23      */
24     private static boolean fillBuffer(CharBuffer buffer)
25     {
26         if (index >= strs.length)
27             return false;
28         
29         String str = strs[index++];
30         for (int i = 0; i < str.length(); i++)
31         {
32             buffer.put(str.charAt(i));
33         }
34         
35         return true;
36     }
37     
38     /**
39      * 從Buffer內把資料拿出來
40      */
41     private static void drainBuffer(CharBuffer buffer)
42     {
43         while (buffer.hasRemaining())
44         {
45             System.out.print(buffer.get());
46         }
47         System.out.println("");
48     }
49     
50     public static void main(String[] args)
51     {
52         CharBuffer cb = CharBuffer.allocate(100);
53         while (fillBuffer(cb))
54         {
55             cb.flip();
56             drainBuffer(cb);
57             cb.clear();
58         }
59     }
60 }

逐一解釋一下:

1、第52行,CharBuffer是一個抽象類,它不能被例項化,因此利用allocate方法來例項化,相當於是一個工廠方法。例項化出來的是HeapCharBuffer,預設大小是100。根據上面的Buffer的類家族圖譜,可以看到每個Buffer的子類都是使用allocate方法來例項化具體的子類的,且例項化出來的都是Heap*Buffer

2、第24行~第36行,每次取String陣列中的一個,利用put方法放置一個資料進入CharBuffer中

3、第55行,呼叫flip方法,這是非常重要的。在緩衝區被寫滿後,必須將其清空,但是如果現在在通道上直接執行get()方法,那麼它將從我們剛剛插入的有用資料之外取出未定義資料;如果此時將位置重新設定為0,就會從正確的位置開始獲取資料,但是如何知道何時到達我們所插入資料末端呢?這就是上界屬性被引入的目的----上界屬性指明瞭緩衝區有效內容的末端。因此,在讀取資料的時候我們需要做兩件事情:

(1)將上界屬性limit設定為當前位置    (2)將位置position設定為0

這兩步操作,JDK API給開發者提供了一個filp()方法來完成,flip()方法將一個能夠繼續新增資料元素的填充狀態的緩衝區翻轉成一個準備讀出元素的釋放狀態,因此每次準備讀出元素前,都必須呼叫一次filp()方法

4、第41行~第48行,每次先判斷一下是否已經達到緩衝區的上界,若存在則呼叫get()方法獲取到此元素,get()方法會自動移動下標position

5、第57行,對Buffer的操作完成之後,呼叫clear()方法將所有屬性迴歸原位,但是clear()方法並不會改變緩衝區中的任何資料

 

緩衝區比較

緩衝區的比較即equals方法,緩衝區的比較並不像我們想像得這麼簡單,兩個緩衝區裡面的元素一樣就是相等,兩個緩衝區相等必須滿足以下三個條件:

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

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

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

如果不滿足上面三個條件,則返回false。下面兩幅圖演示了兩個緩衝區相等和不相等的場景,首先是兩個屬性不同的緩衝區也可以相等:

然後是兩個屬性相同但是被等為不相等的緩衝區:

 

 

批量移動資料

緩衝區的設計目的就是為了能夠高效地傳輸資料。一次移動一個資料元素,其實並不高效,如在下面的程式清單中所看到的那樣,Buffer API提供了向緩衝區內外批量移動資料元素的函式:

public abstract class CharBuffer
    extends Buffer
    implements Comparable<CharBuffer>, Appendable, CharSequence, Readable
{
    ...
    public CharBuffer get(char[] dst){...}
    public CharBuffer get(char[] dst, int offset, int length){...}
    public final CharBuffer put(char[] src){...}
    public CharBuffer put(char[] src, int offset, int length){...}
    public CharBuffer put(CharBuffer src){...}
    public final CharBuffer put(String src){...}
    public CharBuffer put(String src, int start, int end){...}
    ...      
}

其實這種批量移動的合成效果和前文的迴圈在底層實現上是一樣的,但是這些方法可能高效得多,因為這種緩衝區實現能夠利用原生程式碼或其他的優化來移動資料。

 

位元組緩衝區

位元組緩衝區和其他緩衝區型別最明顯的不同在於,它們可能成為通道所執行I/O的源頭或目標,如果對NIO有了解的朋友們一定知道,通道只接收ByteBuffer作為引數。

如我們所知道的,作業系統在記憶體區域進行I/O操作,這些記憶體區域,就作業系統方面而言,是相連的位元組序列。於是,毫無疑問,只有位元組緩衝區有資格參與I/O操作。也請回想一下作業系統會直接存取程式----在本例中是JVM程式的記憶體空間,以傳輸資料。這也意味著I/O操作的目標記憶體區域必須是連續的位元組序列,在JVM中,位元組陣列可能不會在記憶體中連續儲存,或者無用儲存單元收集可能隨時對其進行移動。在Java中,陣列是物件,而資料儲存在物件中的方式在不同的JVM實現中各有不同。

出於這一原因,引入了直接緩衝區的概念。直接緩衝區被用於與通道和固有I/O執行緒互動,它們通過使用固有程式碼來告知作業系統直接釋放或填充記憶體區域,對用於通道直接或原始存取的記憶體區域中的位元組元素的儲存盡了最大的努力。

直接位元組緩衝區通常是I/O操作最好的選擇。在設計方面,它們支援JVM可用的最高效I/O機制,非直接位元組緩衝區可以被傳遞給通道,但是這樣可能導致效能損耗,通常非直接緩衝不可能成為一個本地I/O操作的目標,如果開發者向一個通道中傳遞一個非直接ByteBuffer物件用於寫入,通道可能會在每次呼叫中隱含地進行下面的操作:

1、建立一個臨時的直接ByteBuffer物件

2、將非直接緩衝區的內容複製到臨時緩衝中

3、使用臨時緩衝區執行低層次I/O操作

4、臨時緩衝區物件離開作用於,並最終成為被回收的無用資料

這可能導致緩衝區在每個I/O上覆制併產生大量物件,而這種事都是我們極力避免的。

直接緩衝區是I/O的最佳選擇,但可能比建立非直接緩衝區要花費更高的成本。直接緩衝區使用的記憶體是通過呼叫本地作業系統的程式碼分配的,繞過了標準JVM堆疊。建立和銷燬直接緩衝區會明顯比具有堆疊的緩衝區更極愛破費,這取決於主作業系統以及JVM實現。直接緩衝區的記憶體區域不受無用儲存單元收集支配,因為它們位於標準JVM堆疊之外。

直接ByteBuffer是通過呼叫具有所需容量的ByteBuffer.allocateDirect()函式產生的:

public abstract class ByteBuffer
    extends Buffer
    implements Comparable<ByteBuffer>
{
    ...
    public static ByteBuffer allocateDirect(int capacity)
    {
        return new DirectByteBuffer(capacity);
    }
    ...
}

相關文章