【死磕 NIO】— 深入分析Buffer
- 2021 年 11 月 16 日
- 筆記
- 【死磕 Java】--- 死磕 NIO, 死磕 Java, 死磕 NIO
大家好,我是大明哥,今天我們來看看 Buffer。

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

- capacity
capacity 表示容量,Buffer 是一個記憶體塊,其存儲數據的最大大小就是 capacity。我們不斷地往 Buffer 中寫入數據,當 Buffer 被寫滿後也就是存儲的數據達到 capacity 了就需要將其清空,才能繼續寫入數據。
- position
position 的含義取決於 Buffer 處於寫模式還是讀模式:
-
如果是寫模式,則寫入的地方就是所謂的 position,其初始值是 0,最大值是 capacity – 1,當往 Buffer 中寫入一個數據時,position 就會向前移動到下一個待寫入的位置。
-
如果是讀模式,則讀取數據的地方就是 position。當執行
flip()將 buffer 從寫模式切換到讀模式時,position 會被重置為 0,隨著數據不斷的讀取,position 不斷地向前移,直到 limit。 -
limit
與 position 一樣,limit 的含義也取決於 Buffer 處於何種模式:
-
寫模式:當 Buffer 處於寫模式時,limit 是指能夠往 Buffer 中寫入多少數據,其值等於 capacity
-
讀模式:當 Buffer 處於讀模式時,limit 表示能夠從 Buffer 中最多能夠讀取多少數據出來,所以當 Buffer 從寫模式切換到讀模式時,limit 會被設置寫模式下的 position 的值
-
mark
mark 僅僅只是一個標識,可以通過 mark() 方法進行設置,設置值為當前的 position
Buffer 方法
Buffer 提供了一系列的方法用來操作它,比如 clear() 用來清空緩衝區,filp() 用來讀切換等等方法,下面將依次演示 Buffer 的主要方法,包含從 Buffer 獲取實例、寫入數據、讀取數據、重置等等一個系列的操作流程,同時將 position、limit 兩個參數列印出來,便於我們更好地理解 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) 位元組的記憶體空間。需要注意的是 allocate() 裡面參數並不是位元組數,而是寫入對象的數量,比如上面實例參數是 10 ,表明我們可以寫 10 個 double 對象。
結果如下:
================= allocate 10 後 =================
capacity = 10
position = 0
limit = 10
此時,Buffer 的情況如下:

put()
調用 allocate() 分配記憶體後,得到 DoubleBuffer 實例對象,該對象目前處於寫模式,我們可以通過 put() 方法向 Buffer 裡面寫入數據。
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 個元素,此時,各自參數值如下:
================= 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 中存儲數據後,這時 Buffer 仍然處於寫模式狀態,在寫模式狀態下我們是不能直接從 Buffer 中讀取數據的,需要調用 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 的參數發生了微秒的變化:position = 0,limit = 5。前面說過在讀模式下,limit 代表是 Buffer 的可讀長度,它等於寫模式下的 position,而 position 則是讀的位置。
flip() 方法主要是將 Buffer 從寫模式切換為讀模式,其調整的規則如下:
-
設置可讀的長度 limit。將寫模式寫的 Buffer 中內容的最後位置 position 值變成讀模式下的 limit 位置值,新的 limit 值作為讀越界位置
-
設置讀的起始位置。將 position 的值設置為 0 ,表示從 0 位置處開始讀
-
如果之前有 mark 保存的標記位置,也需要消除,因為那是寫模式下的 mark 標記
調動 flip() 後,該 Buffer 情況如下:

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

我們知道 limit 表明當前 Buffer 最大可讀位置,buffer 也是一邊讀,position 位置一邊往前移動,那如果越界讀取呢?
System.out.println("讀取第 3 個位置的數據:" + buffer.get());
System.out.println("讀取第 4 個位置的數據:" + buffer.get());
System.out.println("讀取第 5 個位置的數據:" + buffer.get());
System.out.println("讀取第 6 個位置的數據:" + buffer.get());
System.out.println("讀取第 7 個位置的數據:" + buffer.get());
limit = 5,6 、7 位置明顯越界了,如果越界讀取,Buffer 會拋出 BufferUnderflowException,如下:
讀取第 3 個位置的數據:3.0
讀取第 4 個位置的數據:4.0
讀取第 5 個位置的數據: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 是隨著讀取的進度一直往前移動的,那如果我想在讀取一遍數據呢?使用 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() 後,我們發現 position 的值變成了 0,limit 值變成了 10,也就是 Buffer 被清空了,回歸到最初始狀態。但是裡面的數據仍然是存在的,只是沒有標記哪些數據是已讀,哪些為未讀。

- compact()
compact() 方法也可以將 Buffer 從讀模式切換到寫模式,它跟 clear() 有一些區別。
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() 區別就在於,它會將所有未讀的數據全部複製到 Buffer 的前面(5次put(),兩次 get()),將 position 設置到這些數據後面,所以此時是從未讀的數據後面開始寫入新的數據,Buffer 情況如下:

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

這些不同的 Buffer 類型代表了不同的數據類型,使得可以通過 Buffer 直接操作如 char、short 等類型的數據而不是位元組數據。這些 Buffer 基本上覆蓋了所有能從 IO 中傳輸的 Java 基本數據類型,其中 MappedByteBuffer 是專門用於記憶體映射的的一種 ByteBuffer,後續會專門介紹。
到這裡 Buffer 也就介紹完畢了,下篇文章將介紹它的協作者 Channel。


