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

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

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

put()
調(diào)用 allocate() 分配內(nèi)存后,得到 DoubleBuffer 實(shí)例對(duì)象,該對(duì)象目前處于寫(xiě)模式,我們可以通過(guò) put() 方法向 Buffer 里面寫(xiě)入數(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());
調(diào)用 put() 往 DoubleBuffer 里面存放 2 個(gè)元素,此時(shí),各自參數(shù)值如下:
================= put 1、2 后 =================
capacity = 10
position = 2
limit = 10
我們看到 position 的值變成了 2 ,指向第三個(gè)可以寫(xiě)入元素的位置。這個(gè)時(shí)候我們?cè)賹?xiě)入 3 個(gè)元素:
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());
得到結(jié)果如下:
================= put 3、4、5 后 =================
capacity = 10
position = 5
limit = 10
此時(shí),position 的值變成 5 ,指向第 6 個(gè)可以寫(xiě)入元素的位置。
該 Buffer 的情況如下:

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

get()
調(diào)用 flip() 將 Buffer 切換為讀模式后,就可以調(diào)用 get() 方法讀取 Buffer 中的數(shù)據(jù)了,get() 讀取數(shù)據(jù)很簡(jiǎn)單,每次從 position 的位置讀取一個(gè)數(shù)據(jù),并且將 position 向前移動(dòng) 1 位。如下:
System.out.println("讀取第 1 個(gè)位置的數(shù)據(jù):" + buffer.get());
System.out.println("讀取第 2 個(gè)位置的數(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ù)調(diào)用 2 次 get() 方法,輸出結(jié)果:
讀取第 1 個(gè)位置的數(shù)據(jù):1.0
讀取第 2 個(gè)位置的數(shù)據(jù):2.0
================= get 2 后 =================
capacity = 10
position = 2
limit = 5
position 的值變成了 2 ,表明它向前移動(dòng)了 2 位,此時(shí),Buffer 如下:

我們知道 limit 表明當(dāng)前 Buffer 最大可讀位置,buffer 也是一邊讀,position 位置一邊往前移動(dòng),那如果越界讀取呢?
System.out.println("讀取第 3 個(gè)位置的數(shù)據(jù):" + buffer.get());
System.out.println("讀取第 4 個(gè)位置的數(shù)據(jù):" + buffer.get());
System.out.println("讀取第 5 個(gè)位置的數(shù)據(jù):" + buffer.get());
System.out.println("讀取第 6 個(gè)位置的數(shù)據(jù):" + buffer.get());
System.out.println("讀取第 7 個(gè)位置的數(shù)據(jù):" + buffer.get());
limit = 5,6 、7 位置明顯越界了,如果越界讀取,Buffer 會(huì)拋出 BufferUnderflowException,如下:
讀取第 3 個(gè)位置的數(shù)據(jù):3.0
讀取第 4 個(gè)位置的數(shù)據(jù):4.0
讀取第 5 個(gè)位置的數(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 是隨著讀取的進(jìn)度一直往前移動(dòng)的,那如果我想在讀取一遍數(shù)據(jù)呢?使用 rewind() 方法,可以進(jìn)行重復(fù)讀。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());
運(yùn)行結(jié)果:
================= rewind 后 =================
capacity = 10
position = 0
limit = 5
可以看到,僅僅只是將 position 的值設(shè)置為了 0,limit 的值保持不變。
clear() 和 compact()
flip() 方法用于將 Buffer 從寫(xiě)模式切換到讀模式,那怎么將 Buffer 從讀模式切換至寫(xiě)模式呢?可以調(diào)用 clear() 和 compact() 兩個(gè)方法。
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());
運(yùn)行結(jié)果如下:
================= clear 后 =================
capacity = 10
position = 0
limit = 10
調(diào)用 clear() 后,我們發(fā)現(xiàn) position 的值變成了 0,limit 值變成了 10,也就是 Buffer 被清空了,回歸到最初始狀態(tài)。但是里面的數(shù)據(jù)仍然是存在的,只是沒(méi)有標(biāo)記哪些數(shù)據(jù)是已讀,哪些為未讀。

compact()
compact() 方法也可以將 Buffer 從讀模式切換到寫(xiě)模式,它跟 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());
運(yùn)行結(jié)果如下:
================= compact 后 =================
capacity = 10
position = 3
limit = 10
可以看到 position 的值為 3,它與 clear() 區(qū)別就在于,它會(huì)將所有未讀的數(shù)據(jù)全部復(fù)制到 Buffer 的前面(5次put(),兩次 get()),將 position 設(shè)置到這些數(shù)據(jù)后面,所以此時(shí)是從未讀的數(shù)據(jù)后面開(kāi)始寫(xiě)入新的數(shù)據(jù),Buffer 情況如下:

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

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