天天看點

【死磕 NIO】— 深入分析Buffer

大家好,我是大明哥,今天我們來看看 Buffer。

【死磕 NIO】— 深入分析Buffer

上面幾篇文章詳細介紹了 IO 相關的一些基本概念,如阻塞、非阻塞、同步、異步的差別,Reactor 模式、Proactor 模式。以下是這幾篇文章的連結,有興趣的同學可以閱讀下:

【死磕NIO】— 阻塞、非阻塞、同步、異步,傻傻分不清楚

【死磕NIO】— 阻塞IO,非阻塞IO,IO複用,信号驅動IO,異步IO,這你真的分的清楚嗎?

【死磕 NIO】— Reactor 模式就一定意味着高性能嗎?

【死磕 NIO】— Proactor模式是什麼?很牛逼嗎?

從這篇文章開始,我們将回歸 NIO 方面的相關知識,首先從 NIO 的三大核心元件說起。

Buffer

Channel

Selector

首先是 Buffer

Buffer 是一個抽象類,主要用作緩沖區,其實質我們可以認為是一個可以寫入資料,然後從中讀取資料的記憶體塊。這塊記憶體被包裝成 NIO Buffer 對象,并提供一系列的方法便于我們通路這塊記憶體。

要了解 Buffer 的工作原理,首先就要了解它的 4 個索引:

capacity:容量

position:位置

limit:界限

mark:标記

capacity 則表示該 Buffer 的容量,而 position 和 limit 的含義取決于 Buffer 處于什麼模式(讀模式或者寫模式),下圖描述讀寫模式下這三種屬性的含義

【死磕 NIO】— 深入分析Buffer

capacity

capacity 表示容量,Buffer 是一個記憶體塊,其存儲資料的最大大小就是 capacity。我們不斷地往 Buffer 中寫入資料,當 Buffer 被寫滿後也就是存儲的資料達到 capacity 了就需要将其清空,才能繼續寫入資料。

position

position 的含義取決于 Buffer 處于寫模式還是讀模式:

如果是寫模式,則寫入的地方就是所謂的 position,其初始值是 0,最大值是 capacity - 1,當往 Buffer 中寫入一個資料時,position 就會向前移動到下一個待寫入的位置。

如果是讀模式,則讀取資料的地方就是 position。當執行 <code>flip()</code> 将 buffer 從寫模式切換到讀模式時,position 會被重置為 0,随着資料不斷的讀取,position 不斷地向前移,直到 limit。

limit

與 position 一樣,limit 的含義也取決于 Buffer 處于何種模式:

寫模式:當 Buffer 處于寫模式時,limit 是指能夠往 Buffer 中寫入多少資料,其值等于 capacity

讀模式:當 Buffer 處于讀模式時,limit 表示能夠從 Buffer 中最多能夠讀取多少資料出來,是以當 Buffer 從寫模式切換到讀模式時,limit 會被設定寫模式下的 position 的值

mark

mark 僅僅隻是一個辨別,可以通過 <code>mark()</code> 方法進行設定,設定值為目前的 position

Buffer 提供了一系列的方法用來操作它,比如 <code>clear()</code> 用來清空緩沖區,<code>filp()</code> 用來讀切換等等方法,下面将依次示範 Buffer 的主要方法,包含從 Buffer 擷取執行個體、寫入資料、讀取資料、重置等等一個系列的操作流程,同時将 position、limit 兩個參數列印出來,便于我們更好地了解 Buffer。

要擷取一個 Buffer 對象,首先就要為期配置設定記憶體空間,使用 <code>allocate()</code> 方法配置設定記憶體空間,如下:

這裡配置設定了 <code>10 * sikeof(double)</code> 位元組的記憶體空間。需要注意的是 <code>allocate()</code> 裡面參數并不是位元組數,而是寫入對象的數量,比如上面執行個體參數是 10 ,表明我們可以寫 10 個 double 對象。

結果如下:

此時,Buffer 的情況如下:

【死磕 NIO】— 深入分析Buffer

調用 <code>allocate()</code> 配置設定記憶體後,得到 DoubleBuffer 執行個體對象,該對象目前處于寫模式,我們可以通過 <code>put()</code> 方法向 Buffer 裡面寫入資料。

調用 <code>put()</code> 往 DoubleBuffer 裡面存放 2 個元素,此時,各自參數值如下:

我們看到 position 的值變成了 2 ,指向第三個可以寫入元素的位置。這個時候我們再寫入 3 個元素:

得到結果如下:

此時,position 的值變成 5 ,指向第 6 個可以寫入元素的位置。

該 Buffer 的情況如下:

【死磕 NIO】— 深入分析Buffer

調用 <code>put()</code> 方法向 Buffer 中存儲資料後,這時 Buffer 仍然處于寫模式狀态,在寫模式狀态下我們是不能直接從 Buffer 中讀取資料的,需要調用 <code>flip()</code> 方法将 Buffer 從寫模式切換為讀模式。

得到的結果如下:

調用 <code>flip()</code> 方法将 Buffer 從寫模式切換為讀模式後,Buffer 的參數發生了微秒的變化:position = 0,limit = 5。前面說過在讀模式下,limit 代表是 Buffer 的可讀長度,它等于寫模式下的 position,而 position 則是讀的位置。

<code>flip()</code> 方法主要是将 Buffer 從寫模式切換為讀模式,其調整的規則如下:

設定可讀的長度 limit。将寫模式寫的 Buffer 中内容的最後位置 position 值變成讀模式下的 limit 位置值,新的 limit 值作為讀越界位置

設定讀的起始位置。将 position 的值設定為 0 ,表示從 0 位置處開始讀

如果之前有 mark 儲存的标記位置,也需要消除,因為那是寫模式下的 mark 标記

調動 <code>flip()</code> 後,該 Buffer 情況如下:

【死磕 NIO】— 深入分析Buffer

調用 <code>flip()</code> 将 Buffer 切換為讀模式後,就可以調用 <code>get()</code> 方法讀取 Buffer 中的資料了,<code>get()</code> 讀取資料很簡單,每次從 position 的位置讀取一個資料,并且将 position 向前移動 1 位。如下:

連續調用 2 次 <code>get()</code> 方法,輸出結果:

position 的值變成了 2 ,表明它向前移動了 2 位,此時,Buffer 如下:

【死磕 NIO】— 深入分析Buffer

我們知道 limit 表明目前 Buffer 最大可讀位置,buffer 也是一邊讀,position 位置一邊往前移動,那如果越界讀取呢?

limit = 5,6 、7 位置明顯越界了,如果越界讀取,Buffer 會抛出 BufferUnderflowException,如下:

position 是随着讀取的進度一直往前移動的,那如果我想在讀取一遍資料呢?使用 <code>rewind()</code> 方法,可以進行重複讀。<code>rewind()</code> 也叫做倒帶,就想播放錄音帶一樣,倒回去重新讀。

運作結果:

可以看到,僅僅隻是将 position 的值設定為了 0,limit 的值保持不變。

<code>flip()</code> 方法用于将 Buffer 從寫模式切換到讀模式,那怎麼将 Buffer 從讀模式切換至寫模式呢?可以調用 <code>clear()</code> 和 <code>compact()</code> 兩個方法。

clear()

運作結果如下:

調用 <code>clear()</code> 後,我們發現 position 的值變成了 0,limit 值變成了 10,也就是 Buffer 被清空了,回歸到最初始狀态。但是裡面的資料仍然是存在的,隻是沒有标記哪些資料是已讀,哪些為未讀。

【死磕 NIO】— 深入分析Buffer

compact()

<code>compact()</code> 方法也可以将 Buffer 從讀模式切換到寫模式,它跟 <code>clear()</code> 有一些差別。

可以看到 position 的值為 3,它與 <code>clear()</code> 差別就在于,它會将所有未讀的資料全部複制到 Buffer 的前面(5次<code>put()</code>,兩次 <code>get()</code>),将 position 設定到這些資料後面,是以此時是從未讀的資料後面開始寫入新的資料,Buffer 情況如下:

【死磕 NIO】— 深入分析Buffer

調用 <code>mark()</code> 方法可以标志一個指定的位置(即設定 mark 的值),之後調用 <code>reset()</code> 時,position 又會回到之前标記的位置。

通過上面的步驟示範,我想小夥伴基本上已經掌握了 Buffer 的使用方法,這裡簡要總結下,使用 Buffer 的步驟如下:

将資料寫入 Buffer 中

調用 <code>flip()</code> 方法,将 Buffer 切換為讀模式

從 Buffer 中讀取資料

調用 <code>clear()</code> 或者 <code>compact()</code> 方法将 Buffer 切換為寫模式

在 NIO 中主要有 8 中 Buffer,分别如下:

ByteBuffer

CharBuffer

DoubleBuffer

FloatBuffer

IntBuffer

LongBuffer

ShortBuffer

MappedByteBuffer

其 UML 類圖如下:

【死磕 NIO】— 深入分析Buffer

這些不同的 Buffer 類型代表了不同的資料類型,使得可以通過 Buffer 直接操作如 char、short 等類型的資料而不是位元組資料。這些 Buffer 基本上覆寫了所有能從 IO 中傳輸的 Java 基本資料類型,其中 MappedByteBuffer 是專門用于記憶體映射的的一種 ByteBuffer,後續會專門介紹。

到這裡 Buffer 也就介紹完畢了,下篇文章将介紹它的協作者 Channel。

PS:如果你覺得文章對你有所幫助,别忘了推薦或者分享,因為有你的支援,才是我續寫下篇的動力和源泉!

作者:chenssy。一個專注于【死磕 Java】系列創作的男人

出處:https://www.cnblogs.com/chenssy/p/15564349.html

作者個人網站:https://www.cmsblogs.com/。專注于 Java 優質系列文章分享,提供一站式 Java 學習資料

目前死磕系列包括:

    1. 【死磕 Java 并發】:https://www.cmsblogs.com/category/1391296887813967872(已完成)

    2.【死磕 Spring 之 IOC】:https://www.cmsblogs.com/category/1391374860344758272(已完成)

    3.【死磕 Redis】:https://www.cmsblogs.com/category/1391389927996002304(已完成)

    4.【死磕 Java 基礎】:https://www.cmsblogs.com/category/1411518540095295488

    5.【死磕 NIO】:https://www.cmsblogs.com/article/1435620402348036096

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。

繼續閱讀