NIO Buffer (死磕2)

【正文】JAVA NIO 死磕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

wps6DEB.tmp



這些 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羣文件共享。


瘋狂創客圈:如果說Java是一個武林,這裏的聚集一羣武癡, 交流編程體驗心得
QQ羣鏈接:
瘋狂創客圈QQ羣


無編程不創客,無案例不學習。 一定記得去跑一跑案例哦


JAVA NIO 死磕全目錄


1. JAVA NIO簡介
1.1. NIO 和 OIO 的對比
1.2. 阻塞和非阻塞
1.3. Channel
1.4. selector
1.5. Java NIO Buffer
2. Java NIO Buffer
2.1. Buffer類型的標記屬性
2.1.1. capacity
2.1.2. position
2.1.3. limit
2.1.4. 總結:
2.2. Buffer 類型
2.3. Buffer中的方法
2.3.1. 獲取allocate()方法
2.3.2. 寫put()方法
2.3.3. 讀切換flip()方法
2.3.4. 讀get() 方法
2.3.5. 倒帶rewind()方法
2.3.6. mark( )和reset( )
2.3.7. clear()清空
2.4. Buffer 的使用
2.4.1. 使用的基本步驟
2.4.2. 完整的實例代碼
3. Java NIO Channel
3.1. Java NIO Channel的特點
3.2. Channel類型
3.3. FileChannel
3.4. SocketChannel
3.4.1. 監聽連接
3.4.2. 非阻塞模式
3.5. DatagramChannel
4. NIO Selector
4.1. Selector入門
4.1.1. Selector的和Channel的關係
4.1.2. 可選擇通道(SelectableChannel)
4.1.3. Channel註冊到Selector
4.1.4. 選擇鍵(SelectionKey)
4.2. Selector的使用流程
4.2.1. 創建Selector
4.2.2. 將Channel註冊到Selector
4.2.3. 輪詢查詢就緒操作
4.3. 一個NIO 編程的簡單實例


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章