【死磕 NIO】— 深入分析Buffer
大家好,我是大明哥,今天我們來看看 Buffer。

上面幾篇文章詳細介紹了 IO 相關的一些基本概念,如阻塞、非阻塞、同步、異步的區(qū)別,Reactor 模式、Proactor 模式。以下是這幾篇文章的鏈接,有興趣的同學可以閱讀下:
從這篇文章開始,我們將回歸 NIO 方面的相關知識,首先從 NIO 的三大核心組件說起。
Buffer
Channel
Selector
首先是 Buffer
Buffer
Buffer 是一個抽象類,主要用作緩沖區(qū),其實質我們可以認為是一個可以寫入數(shù)據(jù),然后從中讀取數(shù)據(jù)的內存塊。這塊內存被包裝成 NIO Buffer 對象,并提供一系列的方法便于我們訪問這塊內存。
要理解 Buffer 的工作原理,首先就要理解它的 4 個索引:
capacity:容量
position:位置
limit:界限
mark:標記
capacity 則表示該 Buffer 的容量,而 position 和 limit 的含義取決于 Buffer 處于什么模式(讀模式或者寫模式),下圖描述讀寫模式下這三種屬性的含義

capacity
capacity 表示容量,Buffer 是一個內存塊,其存儲數(shù)據(jù)的最大大小就是 capacity。我們不斷地往 Buffer 中寫入數(shù)據(jù),當 Buffer 被寫滿后也就是存儲的數(shù)據(jù)達到 capacity 了就需要將其清空,才能繼續(xù)寫入數(shù)據(jù)。
position
position 的含義取決于 Buffer 處于寫模式還是讀模式:
如果是寫模式,則寫入的地方就是所謂的 position,其初始值是 0,最大值是 capacity - 1,當往 Buffer 中寫入一個數(shù)據(jù)時,position 就會向前移動到下一個待寫入的位置。
如果是讀模式,則讀取數(shù)據(jù)的地方就是 position。當執(zhí)行
flip()將 buffer 從寫模式切換到讀模式時,position 會被重置為 0,隨著數(shù)據(jù)不斷的讀取,position 不斷地向前移,直到 limit。limit
與 position 一樣,limit 的含義也取決于 Buffer 處于何種模式:
寫模式:當 Buffer 處于寫模式時,limit 是指能夠往 Buffer 中寫入多少數(shù)據(jù),其值等于 capacity
讀模式:當 Buffer 處于讀模式時,limit 表示能夠從 Buffer 中最多能夠讀取多少數(shù)據(jù)出來,所以當 Buffer 從寫模式切換到讀模式時,limit 會被設置寫模式下的 position 的值
mark
mark 僅僅只是一個標識,可以通過 mark() 方法進行設置,設置值為當前的 position
Buffer 方法
Buffer 提供了一系列的方法用來操作它,比如 clear() 用來清空緩沖區(qū),filp() 用來讀切換等等方法,下面將依次演示 Buffer 的主要方法,包含從 Buffer 獲取實例、寫入數(shù)據(jù)、讀取數(shù)據(jù)、重置等等一個系列的操作流程,同時將 position、limit 兩個參數(shù)打印出來,便于我們更好地理解 Buffer。
allocate()
要獲取一個 Buffer 對象,首先就要為期分配內存空間,使用 allocate() 方法分配內存空間,如下:
DoubleBuffer buffer = DoubleBuffer.allocate(10);
System.out.println("================= allocate 10 后 =================");
System.out.println("capacity = " + buffer.capacity());
System.out.println("position = " + buffer.position());
System.out.println("limit = " + buffer.limit());
這里分配了 10 * sikeof(double) 字節(jié)的內存空間。需要注意的是 allocate() 里面參數(shù)并不是字節(jié)數(shù),而是寫入對象的數(shù)量,比如上面實例參數(shù)是 10 ,表明我們可以寫 10 個 double 對象。
結果如下:
================= allocate 10 后 =================
capacity = 10
position = 0
limit = 10
此時,Buffer 的情況如下:

put()
調用 allocate() 分配內存后,得到 DoubleBuffer 實例對象,該對象目前處于寫模式,我們可以通過 put() 方法向 Buffer 里面寫入數(shù)據(jù)。
buffer.put(1);
buffer.put(2);
System.out.println("================= put 1、2 后 =================");
System.out.println("capacity = " + buffer.capacity());
System.out.println("position = " + buffer.position());
System.out.println("limit = " + buffer.limit());
調用 put() 往 DoubleBuffer 里面存放 2 個元素,此時,各自參數(shù)值如下:
================= put 1、2 后 =================
capacity = 10
position = 2
limit = 10
我們看到 position 的值變成了 2 ,指向第三個可以寫入元素的位置。這個時候我們再寫入 3 個元素:
buffer.put(3);
buffer.put(4);
buffer.put(5);
System.out.println("================= put 3、4、5 后 =================");
System.out.println("capacity = " + buffer.capacity());
System.out.println("position = " + buffer.position());
System.out.println("limit = " + buffer.limit());
得到結果如下:
================= put 3、4、5 后 =================
capacity = 10
position = 5
limit = 10
此時,position 的值變成 5 ,指向第 6 個可以寫入元素的位置。
該 Buffer 的情況如下:

flip()
調用 put() 方法向 Buffer 中存儲數(shù)據(jù)后,這時 Buffer 仍然處于寫模式狀態(tài),在寫模式狀態(tài)下我們是不能直接從 Buffer 中讀取數(shù)據(jù)的,需要調用 flip() 方法將 Buffer 從寫模式切換為讀模式。
buffer.flip();
System.out.println("================= flip 后 =================");
System.out.println("capacity = " + buffer.capacity());
System.out.println("position = " + buffer.position());
System.out.println("limit = " + buffer.limit());
得到的結果如下:
================= flip 后 =================
capacity = 10
position = 0
limit = 5
調用 flip() 方法將 Buffer 從寫模式切換為讀模式后,Buffer 的參數(shù)發(fā)生了微秒的變化:position = 0,limit = 5。前面說過在讀模式下,limit 代表是 Buffer 的可讀長度,它等于寫模式下的 position,而 position 則是讀的位置。
flip() 方法主要是將 Buffer 從寫模式切換為讀模式,其調整的規(guī)則如下:
設置可讀的長度 limit。將寫模式寫的 Buffer 中內容的最后位置 position 值變成讀模式下的 limit 位置值,新的 limit 值作為讀越界位置
設置讀的起始位置。將 position 的值設置為 0 ,表示從 0 位置處開始讀
如果之前有 mark 保存的標記位置,也需要消除,因為那是寫模式下的 mark 標記
調動 flip() 后,該 Buffer 情況如下:

get()
調用 flip() 將 Buffer 切換為讀模式后,就可以調用 get() 方法讀取 Buffer 中的數(shù)據(jù)了,get() 讀取數(shù)據(jù)很簡單,每次從 position 的位置讀取一個數(shù)據(jù),并且將 position 向前移動 1 位。如下:
System.out.println("讀取第 1 個位置的數(shù)據(jù):" + buffer.get());
System.out.println("讀取第 2 個位置的數(shù)據(jù):" + buffer.get());
System.out.println("================= get 2 后 =================");
System.out.println("capacity = " + buffer.capacity());
System.out.println("position = " + buffer.position());
System.out.println("limit = " + buffer.limit());
連續(xù)調用 2 次 get() 方法,輸出結果:
讀取第 1 個位置的數(shù)據(jù):1.0
讀取第 2 個位置的數(shù)據(jù):2.0
================= get 2 后 =================
capacity = 10
position = 2
limit = 5
position 的值變成了 2 ,表明它向前移動了 2 位,此時,Buffer 如下:

我們知道 limit 表明當前 Buffer 最大可讀位置,buffer 也是一邊讀,position 位置一邊往前移動,那如果越界讀取呢?
System.out.println("讀取第 3 個位置的數(shù)據(jù):" + buffer.get());
System.out.println("讀取第 4 個位置的數(shù)據(jù):" + buffer.get());
System.out.println("讀取第 5 個位置的數(shù)據(jù):" + buffer.get());
System.out.println("讀取第 6 個位置的數(shù)據(jù):" + buffer.get());
System.out.println("讀取第 7 個位置的數(shù)據(jù):" + buffer.get());
limit = 5,6 、7 位置明顯越界了,如果越界讀取,Buffer 會拋出 BufferUnderflowException,如下:
讀取第 3 個位置的數(shù)據(jù):3.0
讀取第 4 個位置的數(shù)據(jù):4.0
讀取第 5 個位置的數(shù)據(jù):5.0
Exception in thread "main" java.nio.BufferUnderflowException
at java.nio.Buffer.nextGetIndex(Buffer.java:500)
at java.nio.HeapDoubleBuffer.get(HeapDoubleBuffer.java:135)
at com.chenssy.study.nio.BufferTest.main(BufferTest.java:48)
rewind()
position 是隨著讀取的進度一直往前移動的,那如果我想在讀取一遍數(shù)據(jù)呢?使用 rewind() 方法,可以進行重復讀。rewind() 也叫做倒帶,就想播放磁帶一樣,倒回去重新讀。
buffer.rewind();
System.out.println("================= rewind 后 =================");
System.out.println("capacity = " + buffer.capacity());
System.out.println("position = " + buffer.position());
System.out.println("limit = " + buffer.limit());
運行結果:
================= rewind 后 =================
capacity = 10
position = 0
limit = 5
可以看到,僅僅只是將 position 的值設置為了 0,limit 的值保持不變。
clear() 和 compact()
flip() 方法用于將 Buffer 從寫模式切換到讀模式,那怎么將 Buffer 從讀模式切換至寫模式呢?可以調用 clear() 和 compact() 兩個方法。
clear()
buffer.clear();
System.out.println("================= clear 后 =================");
System.out.println("capacity = " + buffer.capacity());
System.out.println("position = " + buffer.position());
System.out.println("limit = " + buffer.limit());
運行結果如下:
================= clear 后 =================
capacity = 10
position = 0
limit = 10
調用 clear() 后,我們發(fā)現(xiàn) position 的值變成了 0,limit 值變成了 10,也就是 Buffer 被清空了,回歸到最初始狀態(tài)。但是里面的數(shù)據(jù)仍然是存在的,只是沒有標記哪些數(shù)據(jù)是已讀,哪些為未讀。

compact()
compact() 方法也可以將 Buffer 從讀模式切換到寫模式,它跟 clear() 有一些區(qū)別。
buffer.compact();
System.out.println("================= compact 后 =================");
System.out.println("capacity = " + buffer.capacity());
System.out.println("position = " + buffer.position());
System.out.println("limit = " + buffer.limit());
運行結果如下:
================= compact 后 =================
capacity = 10
position = 3
limit = 10
可以看到 position 的值為 3,它與 clear() 區(qū)別就在于,它會將所有未讀的數(shù)據(jù)全部復制到 Buffer 的前面(5次put(),兩次 get()),將 position 設置到這些數(shù)據(jù)后面,所以此時是從未讀的數(shù)據(jù)后面開始寫入新的數(shù)據(jù),Buffer 情況如下:

mark() 和 reset()
調用 mark() 方法可以標志一個指定的位置(即設置 mark 的值),之后調用 reset() 時,position 又會回到之前標記的位置。
通過上面的步驟演示,我想小伙伴基本上已經掌握了 Buffer 的使用方法,這里簡要總結下,使用 Buffer 的步驟如下:
將數(shù)據(jù)寫入 Buffer 中
調用
flip()方法,將 Buffer 切換為讀模式從 Buffer 中讀取數(shù)據(jù)
調用
clear()或者compact()方法將 Buffer 切換為寫模式
Buffer 的類型
在 NIO 中主要有 8 中 Buffer,分別如下:
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
MappedByteBuffer
其 UML 類圖如下:

這些不同的 Buffer 類型代表了不同的數(shù)據(jù)類型,使得可以通過 Buffer 直接操作如 char、short 等類型的數(shù)據(jù)而不是字節(jié)數(shù)據(jù)。這些 Buffer 基本上覆蓋了所有能從 IO 中傳輸?shù)?Java 基本數(shù)據(jù)類型,其中 MappedByteBuffer 是專門用于內存映射的的一種 ByteBuffer,后續(xù)會專門介紹。
到這里 Buffer 也就介紹完畢了,下篇文章將介紹它的協(xié)作者 Channel。
