什么是緩沖區
一個緩沖區對象是固定數量的數據的容器,其作用是一個存儲器,或者分段運輸區,在這里數據可被存儲並在之后用於檢索。緩沖區像前篇文章討論的那樣被寫滿和釋放,對於每個非布爾原始數據類型都有一個緩沖區類,盡管緩沖區作用於它們存儲的原始數據類型,但緩沖區十分傾向於處理字節,非字節緩沖區可以再后台執行從字節或到字節的轉換,這取決於緩沖區是如何創建的。
緩沖區的工作與通道緊密聯系。通道是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); } ... }