天天看點

Java NIO Buffer(netty源碼死磕1.2)

【基礎篇】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

Java NIO Buffer(netty源碼死磕1.2)

這些 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