【基礎篇】netty源碼死磕1.2: NIO Buffer
1. Java NIO Buffer
Buffer是一個抽象類,位于java.nio包中,主要用作緩沖區。Buffer緩沖區本質上是一塊可以寫入資料,然後可以從中讀取資料的記憶體。這塊記憶體被包裝成NIO Buffer對象,并提供了一組方法,用來友善的通路該塊記憶體。
注意:Buffer是非線程安全類。
1.1. Buffer類型的标記屬性
Buffer在内部也是利用byte[]作為記憶體緩沖區,隻不過多提供了一些标記變量屬性而已。當多線程通路的時候,可以清楚的知道目前資料的位置。
有三個重要的标記屬性:capacity、position、limit。
除此之外,還有一個标記屬性:mark,可以臨時保持一個特定的position,需要的時候,可以恢複到這個位置。
1.1.1. capacity
作為一個記憶體塊,Buffer有一個固定的大小值,也叫“capacity”。你隻能往裡寫capacity個資料。一旦Buffer滿了,就不能再寫入。
capacity與緩存的資料類型相關。指的不是記憶體的位元組的數量,而是寫入的對象的數量。比如使用的是一個儲存double類型的Buffer(DoubleBuffer),寫入的資料是double類型, 如果其 capacity 是100,那麼我們最多可以寫入100個 double 資料.
capacity一旦初始化,就不能不會改變。
原因是什麼呢?
Buffer對象在初始化時,會按照capacity配置設定内部的記憶體。記憶體配置設定好後,大小就不能變了。配置設定記憶體時,一般使用Buffer的抽象子類ByteBuffer.allocate()方法,實際上是生成ByteArrayBuffer類。
1.1.2. position
position表示目前的位置。position在Buffer的兩種模式下的值是不同的。
讀模式下的position的值為:
當讀取資料時,也是從position位置開始讀。當将Buffer從寫模式切換到讀模式,position會被重置為0。當從Buffer的position處讀取資料時,position向前移動到下一個可讀的位置。
寫模式下的position的值為:
在寫模式下,當寫資料到Buffer中時,position表示目前的寫入位置。初始的position值為0,position最大可為capacity – 1。
每當一個資料(byte、long等)寫到Buffer後, position會向後移動到下一個可插入資料的可寫的位置。
1.1.3. limit
limit表示最大的限制。在Buffer的兩種模式下,limit的值是不同的。
讀模式下的limit的值為:
讀模式下,Buffer的limit表示最多能從Buffer裡讀多少資料。當Buffer從寫切換到讀模式時,limit的值,設定成寫模式的position 值,也是是寫模式下之前寫入的數量值。
舉一個簡單的例子,說明一下讀模式下的limit值:
先向Buffer寫資料,Buffer在寫模式。每寫入一個資料,position向後面移動一個位置,值加一。假定寫入了5個數,當寫入完成後,position的值為5。這時,就可以讀取資料了。當開始讀取資料時,Buffer切換到讀模式。limit的值,先會被設定成寫入資料時的position值。這裡是5,表示可以讀取的最大限制是5個數。
寫模式下的limit的值為:
limit表示表示可以寫入的資料最大限制。在切換成寫模式時,limit的值會被更改,設定成Buffer的capacity,為Buffer的容量。
1.1.4. 總結:
在Buffer的四個屬性之間,有一個簡單的數量關系,如下:
capacity>=limit>=position>=mark>=0
用一個表格,對着4個屬性的進行一下對比:
屬性 | 描述 |
capacity | 容量,即可以容納的最大資料量;在緩沖區建立時被設定并且不能改變 |
limit | 上界,緩沖區中目前資料量 |
position | 位置,下一個要被讀或寫的元素的索引 |
mark(位置标記) | 調用mark(pos)來設定mark=pos,再調用reset()可以讓position恢複到标記的位置即position=mark |
1.2. Buffer 類型
在NIO中主要有八種緩沖區類,分别如下:
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
MappedByteBuffer
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnL4kTNyUDM1UjNx0yM2cDN0MTNwITMyATM4EDMy0CO5MTN4QTMvwFMxgTMwIzLchTOzUDO0EzLcd2bsJ2Lc12bj5ycn9Gbi52YugTMwIzZtl2Lc9CX6MHc0RHaiojIsJye.png)
這些 Buffer 覆寫了能從 IO 中傳輸的所有的 Java 基本資料類型。其中MappedByteBuffer是專門用于記憶體映射的一種ByteBuffer)。
1.3. Buffer中的方法
本節結合Buffer的幾個方法,做了一個完整的執行個體,包含了從Buffer執行個體的擷取、寫入、讀取、重複讀、标記和重置等一個系列操作的完整流程。
1.3.1. 擷取allocate()方法
為了擷取一個 Buffer 對象,我們首先需要配置設定記憶體空間。配置設定記憶體空間使用allocate()方法。
public static void allocatTest()
{
byteBuffer = IntBuffer.allocate(20);
Logger.info("------------after allocate------------------");
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}
byteBuffer = IntBuffer.allocate(20);
這裡我們配置設定了20* sizeof(int)位元組的記憶體空間.
輸出的結果如下:
main |> 配置設定記憶體
allocatTest |> ------------after allocate------------------
allocatTest |> position=0
allocatTest |> limit=20
allocatTest |> capacity=20
通過結果,可以看到Buffer屬性的值。
1.3.2. 寫put()方法
調用allocate配置設定記憶體後,buffer處于寫模式。可以通過buffer的put方法寫入資料。put方法有一個要求,需要寫入的資料類型與Buffer的類型一緻。
接着前面的例子,繼續上寫入的執行個體代碼:
public static void putTest()
{
for (int i = 0; i < 5; i++)
{
byteBuffer.put(i);
}
Logger.info("------------after put------------------");
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}
寫入5個元素後,輸出的結果為:
main |> 寫入
putTest |> ------------after putTest------------------
putTest |> position=5
putTest |> limit=20
putTest |> capacity=20
調用了put方法後,buffer處于寫模式。寫入5個資料後,可以看到,position 變成了5,指向了第6個可以寫入的元素位置。
除了在建立的buffer之後,如何将buffer切換成寫模式呢?
調用 Buffer.clear() 清空或 Buffer.compact()壓縮方法,可以将 Buffer 轉換為寫模式。
1.3.3. 讀切換flip()方法
put方法寫入資料之後,可以直接從buffer中讀嗎?
呵呵,不能。
還需要調用filp()走一個轉換的工作。flip()方法是Buffer的一個模式轉變的重要方法。簡單的說,是寫模式翻轉成讀模式——寫轉讀。
接着前面的例子,繼續上flip()方法的例子代碼:
public static void flipTest()
{
byteBuffer.flip();
Logger.info("------------after flip ------------------");
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}
接着上一步的寫入,在調用flip之後,buffer的屬性有一些奇妙的變化。
運作上面的程式,輸出如下:
main |> 翻轉
flipTest |> ------------after flipTest ------------------
flipTest |> position=0
flipTest |> limit=5
flipTest |> capacity=20
注意到沒有,position從前一個小節的5,變成了0。而limit的儲存了之前的position,從20變成5。
這是為什麼呢? 先看其源碼,Buffer.flip()方法的源碼如下:
public final Buffer flip() {
limit = position;
position = 0;
mark = UNSET_MARK;
return this;
}
解釋一下啊,flip()方法主要是從讀模式切換成寫模式,調整的規則是:
(1)首先設定可讀的長度limit。将寫模式下的Buffer中内容的最後位置position值變為讀模式下的limit位置值,新的limit值作為讀越界位置;
(2)其次設定讀的起始位置。将當position值置為0,表示從0位置開始讀。轉換後重頭開始讀。
(3)如果之前有mark儲存的标記位置,還要消除。因為那是寫模式下的mark标記。
1.3.4. 讀get() 方法
get()讀資料很簡單,每次從postion的位置讀取一個資料,并且進行相應的buffer屬性的調整。
接着前面的例子,繼續上讀取buffer的例子代碼:
public static void getTest()
{
Logger.info("------------after &getTest 2------------------");
for (int i = 0; i < 2; i++)
{
int j = byteBuffer.get();
Logger.info("j = " + j);
}
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
Logger.info("------------after &getTest 3------------------");
for (int i = 0; i < 3; i++)
{
int j = byteBuffer.get();
Logger.info("j = " + j);
}
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}
先讀2個,再讀3個,輸出的Buffer屬性值如下:
main |> 讀取
getTest |> ------------after &getTest 2------------------
getTest |> j = 0
getTest |> j = 1
getTest |> position=2
getTest |> limit=5
getTest |> capacity=20
getTest |> ------------after &getTest 3------------------
getTest |> j = 2
getTest |> j = 3
getTest |> j = 4
getTest |> position=5
getTest |> limit=5
getTest |> capacity=20
讀完之後,緩存的position 值變成了一個沒有資料的元素位置,和limit的值相等,已經不能在讀了。
讀完之後,是否可以直接寫資料呢?
不能。一旦讀取了所有的 Buffer 資料,那麼我們必須清理 Buffer,讓其重新可寫,可以調用 Buffer.clear() 或 Buffer.compact()。
1.3.5. 倒帶rewind()方法
已經讀完的資料,需要再讀一遍,可以直接使用get方法嗎?
答案是,不能。怎麼辦呢?
使用rewind() 方法,可以進重複讀的設定。rewind()也叫倒帶,就像播放錄音帶一樣,倒回去,重新播放。
接着前面的例子,繼續上重複讀的例子代碼:
public static void rewindTest()
{
byteBuffer.rewind();
Logger.info("------------after flipTest ------------------");
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}
執行個體的結果如下:
main |> 重複讀
rewindTest |> ------------after flipTest ------------------
rewindTest |> position=0
rewindTest |> limit=5
rewindTest |> capacity=20
flip()方法主要是調整Buffer的 position 屬性,調整的規則是:
(1)position設回0,是以你可以重讀Buffer中的所有資料;
(2)limit保持不變,資料量還是一樣的,仍然表示能從Buffer中讀取多少個元素。
Buffer.rewind()方法的源碼如下:
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
看到了實作的源碼應該就會清楚flip()的作用了。rewind()方法與flip()很相似,差別在于rewind()不會影響limit,而flip()會重設limit屬性值。
1.3.6. mark( )和reset( )
Buffer.mark()方法将目前的 position 的值儲存起來,放在mark屬性中,讓mark屬性記住目前位置,之後可以調用Buffer.reset()方法将 position 的值恢複回來。
Buffer.mark()和Buffer.reset()方法是一一配套使用的。都是需要操作mark屬性。
在重複讀的執行個體代碼中,讀到第3個元素,使用mark()方法,設定一下mark 屬性,儲存為第3個元素的位置。
下面上執行個體,示範一下mark和reset的結合使用。
執行個體繼續接着上面的rewind倒帶後的buffer 狀态,開始reRead重複讀,執行個體代碼如下:
public static void reRead()
{
Logger.info("------------after reRead------------------");
for (int i = 0; i < 5; i++)
{
int j = byteBuffer.get();
Logger.info("j = " + j);
if (i == 2)
{
byteBuffer.mark();
}
}
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}
然後接着上一段reset()執行個體代碼,如下:
public static void afterReset()
{
Logger.info("------------after reset------------------");
byteBuffer.reset();
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}
上面我們調用 mark() 方法将目前的 position 儲存起來(在讀模式,是以儲存的是讀的 position)。接着使用 reset() 恢複原來的讀 position,是以讀 position 就為3,可以再次開始從第2個元素讀取資料.
輸出的結果是:
afterReset |> ------------after reset------------------
afterReset |> position=3
afterReset |> limit=5
afterReset |> capacity=20
調用reset之後,position的值為3,表示可以從第三個元素開始讀。
Buffer.mark()和Buffer.reset()其實很簡答,其源碼如下:
public final Buffer mark() {
mark = position;
return this;
}
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
1.3.7. clear()清空
clear()方法的作用有兩種:
(1)寫模式下,當一個 buffer 已經寫滿資料時,調用 clear()方法,切換成讀模式,可以從頭讀取 buffer 的資料;
(2)讀模式下,調用 clear()方法,将buffer切換為寫模式,将postion為清零,limit設定為capacity最大容量值,可以一直寫入,直到buffer寫滿。
接着上面的執行個體,使用執行個體代碼,示範一下clear方法。
代碼如下:
public static void clearDemo()
{
Logger.info("------------after clear------------------");
byteBuffer.clear();
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}
運作之後,結果如下:
main |> 清空
clearDemo |> ------------after clear------------------
clearDemo |> position=0
clearDemo |> limit=20
clearDemo |> capacity=20
在clear()之前,buffer是在讀模式下。clear()之後,可以看到,清空了position 的值,設定為起始位置。
clear 方法源碼:
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
根據源碼我們可以知道,clear 将 positin 設定為0,将 limit 設定為 capacity。
1.4. Buffer 的使用
1.4.1. 使用的基本步驟
總結一下,使用 NIO Buffer 的步驟如下:
一:将資料寫入到 Buffer 中;
二:調用 Buffer.flip()方法,将 NIO Buffer 轉換為讀模式;
三:從 Buffer 中讀取資料;
四:調用 Buffer.clear() 或 Buffer.compact()方法,将 Buffer 轉換為寫模式。
當我們将資料寫入到 Buffer 中時,Buffer 會記錄我們已經寫了多少的資料;當我們需要從 Buffer 中讀取資料時,必須調用 Buffer.flip()将 Buffer 切換為讀模式。
1.4.2. 完整的執行個體代碼
package com.crazymakercircle.iodemo.base;
import com.crazymakercircle.util.Logger;
import java.nio.IntBuffer;
public class BufferDemo
{
static IntBuffer byteBuffer = null;
public static void allocatTest()
{
byteBuffer = IntBuffer.allocate(20);
Logger.info("------------after allocate------------------");
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}
public static void putTest()
{
for (int i = 0; i < 5; i++)
{
byteBuffer.put(i);
}
Logger.info("------------after putTest------------------");
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}
public static void flipTest()
{
byteBuffer.flip();
Logger.info("------------after flipTest ------------------");
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}
public static void rewindTest()
{
byteBuffer.rewind();
Logger.info("------------after flipTest ------------------");
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}
public static void getTest()
{
Logger.info("------------after &getTest 2------------------");
for (int i = 0; i < 2; i++)
{
int j = byteBuffer.get();
Logger.info("j = " + j);
}
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
Logger.info("------------after &getTest 3------------------");
for (int i = 0; i < 3; i++)
{
int j = byteBuffer.get();
Logger.info("j = " + j);
}
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}
public static void reRead()
{
Logger.info("------------after reRead------------------");
for (int i = 0; i < 5; i++)
{
int j = byteBuffer.get();
Logger.info("j = " + j);
if (i == 2)
{
byteBuffer.mark();
}
}
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}
public static void afterReset()
{
Logger.info("------------after reset------------------");
byteBuffer.reset();
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}
public static void clearDemo()
{
Logger.info("------------after clear------------------");
byteBuffer.clear();
Logger.info("position=" + byteBuffer.position());
Logger.info("limit=" + byteBuffer.limit());
Logger.info("capacity=" + byteBuffer.capacity());
}
public static void main(String[] args)
{
Logger.info("配置設定記憶體");
allocatTest();
Logger.info("寫入");
putTest();
Logger.info("翻轉");
flipTest();
Logger.info("讀取");
getTest();
Logger.info("重複讀");
rewindTest();
reRead();
Logger.info("make&reset寫讀");
afterReset();
Logger.info("清空");
clearDemo();
}
}
源碼:
代碼工程: JavaNioDemo.zip
下載下傳位址:在瘋狂創客圈QQ群檔案共享。
無程式設計不創客,無案例不學習。瘋狂創客圈,一大波高手正在交流、學習中!
瘋狂創客圈 Netty 死磕系列 10多篇深度文章: 【部落格園 總入口】 QQ群:104131248