Java NIO Buffer 分析

引言

JDK 1.4 之后引入了NIO(New IO或 Non Blocking IO),我覺的可以稱其為New IO,因為NIO基本重寫所有標準IO的API,完全可以替代標準的Java IO API。并且NIO支持面向緩沖區(qū)(Buffer)的、基于通道(Channel)的IO操作,可以以更加高效的方式進行文件的讀寫操作。

NIO的核心主要包括3部分

  • Buffer 緩沖區(qū)
  • Channel 通道
    • 文件通道
    • Socket通道
  • Selector 選擇器

本篇文章會對NIOBuffer緩沖區(qū)的使用及實現(xiàn)原理做分析

體系

Java NIO 提供了以下幾種Buffer類型可供使用

  1. ByteBuffer
  2. MappedByteBuffer
  3. CharBuffer
  4. DoubleBuffer
  5. FloatBuffer
  6. IntBuffer
  7. LongBuffer
  8. ShortBuffer

這些Buffer的名字已經(jīng)非常清晰的指明了這些Buffer所各自承載的不同數(shù)據(jù)類型

下面的分析我都將以ByteBuffer為例子

基本使用

ByteBuffer的使用比較簡單,但若不理解其內(nèi)部實現(xiàn)原理,也非常容易搞混。我們先來介紹其簡單的使用

創(chuàng)建緩沖區(qū)

創(chuàng)建緩沖區(qū)主要有兩種常用方式

  • allocate 開辟指定大小的緩沖區(qū)
  • wrap 使用字節(jié)數(shù)組開辟新的緩沖區(qū),對緩沖區(qū)的修改將導(dǎo)致數(shù)組被修改。新緩沖區(qū)的容量(position)和限制(limit)將為該數(shù)組的長度
// 通過 allocate 創(chuàng)建
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

// 通過 wrap 創(chuàng)建
byte [] buf = new byte[1024];
ByteBuffer.wrap(buf);

讀寫數(shù)據(jù)

使用ByteBuffer讀寫數(shù)據(jù)比較簡單

寫數(shù)據(jù)

往Buffer中寫數(shù)據(jù)只需要調(diào)用put方法,該方法有多個重載,可以以不同的方式寫數(shù)據(jù)
這里只列舉3個

  • put(byte b) 在當前位置將給定字節(jié)寫入此緩沖區(qū)
  • put(int i, byte b) 在指定位置將給定字節(jié)寫入此緩沖區(qū)
  • put(byte[] src, int offset, int length) 將字節(jié)從給定源數(shù)組傳輸?shù)酱司彌_區(qū)。如果從數(shù)組復(fù)制的字節(jié)比數(shù)大于剩余的緩沖區(qū)容量,會拋出BufferOverflowException異常

讀數(shù)據(jù)

從Buffer中讀取數(shù)據(jù)只需要調(diào)用get方法,該方法同樣有多個重載,可以以不同的方式讀數(shù)據(jù)

  • get() 獲取當前位置的單個字節(jié)
  • get(int i) 獲取指定位置的單個字節(jié)
  • get(byte[] dst, int offset, int length) 從此緩沖區(qū)中獲取字節(jié)并傳輸?shù)浇o定的目標數(shù)組中,如果緩沖區(qū)中的剩余字節(jié)少于滿足請求所需的字節(jié)數(shù),會拋出BufferUnderflowException異常

下面是一個拋出BufferUnderflowException的例子

ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
String s =  "hello";
byteBuffer.put(s.getBytes());
byteBuffer.flip(); // 翻轉(zhuǎn) 下文會提到
byte [] bufs = new byte[9]; // 緩沖區(qū)實際只有5個字節(jié)大小,大于此大小就會拋出異常
byteBuffer.get(bufs); // 拋出異常

flip 方法

上文拋出BufferUnderflowException的例子,不知道大家有沒發(fā)現(xiàn),我們的代碼在讀寫轉(zhuǎn)換時用了flip方法來進行讀寫操作的轉(zhuǎn)換,緩沖區(qū)在進行讀寫轉(zhuǎn)換的時候必須調(diào)用flip方法,那么為何要這么做呢?下面我將會對flip方法的內(nèi)部實現(xiàn)原理進行分析,在分析之前,我們有必要先認識一下Buffercapacity position limit mark四個變量屬性

  • capacity 表示緩沖區(qū)的容量,大小不可變且不可為負
  • position 下一個要讀取或?qū)懭氲脑氐奈恢盟饕?/li>
  • limit 下一個不應(yīng)該被讀取或?qū)懭氲脑氐奈恢盟饕?/li>
  • mark 標記位,可以通過這個標記位返回此位置

四者的大小關(guān)系 mark <= position <= limit <= capacity

知道了這四個變量的含義之后,我們再來根據(jù)例子來看看一步一步往下分析吧

一. 首先,我們通過ByteBuffer.allocate(6)開辟一個大小為6的緩沖區(qū)

// 通過allocate 開辟緩沖區(qū)
public static ByteBuffer allocate(int capacity) {
    if (capacity < 0)
        throw new IllegalArgumentException();
    return new HeapByteBuffer(capacity, capacity);
}

// HeapByteBuffer 初始化
HeapByteBuffer(int cap, int lim) {            // package-private
      // 這里前四個參數(shù)依次是 mark position limit capacity
    super(-1, 0, lim, cap, new byte[cap], 0);
}

通過查看allocate方法的源碼,可知其內(nèi)部初始結(jié)構(gòu)如下圖(Buffer內(nèi)部其實是數(shù)組實現(xiàn),下標從0開始)

  • mark = -1
  • position = 0
  • limit = capacity = 6
image.png

二. 然后我們現(xiàn)在通過put方法加入1個字節(jié)數(shù)據(jù)
此時的內(nèi)部結(jié)構(gòu)如下圖

  • mark = -1
  • position = 1
  • limit = capacity = 6
image.png

三. 加一個太少了,我們再添加3個字節(jié)數(shù)據(jù)至緩沖區(qū)中
此時的內(nèi)部結(jié)構(gòu)如下圖

  • mark = -1
  • position = 4
  • limit = capacity = 6
image.png

四. 現(xiàn)在數(shù)據(jù)已經(jīng)全部加完了,我們準備讀數(shù)據(jù)了,必須要調(diào)用flip方法
先來看看flip方法的內(nèi)部實現(xiàn)

public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}

flip內(nèi)部其實是將 position的值賦給了limit,并將position位置歸0,如下圖顯示

image.png

五. 成功翻轉(zhuǎn)之后,就可以往外讀數(shù)據(jù)了,假設(shè)往外讀4個數(shù)據(jù)
此時內(nèi)部結(jié)構(gòu)如下圖,并且 position = limit了,達到了最大可讀字節(jié)數(shù)

  • mark = -1
  • position = 4
  • limit = 4
  • capacity = 6


    image.png

通過上面這個例子,對flip方法的翻轉(zhuǎn)邏輯已經(jīng)進行了非常詳細的分析,并且可以得出結(jié)論capacity的是絕對不變的,不會隨著讀寫操作而改變。mark變量的值會在下文reset mark 方法中用到

reset mark 方法

顧名思義,mark就是打標記的意思,reset表示重置的意思。這兩個操做是緊密聯(lián)系的,由mark()方法在當前position位置做標記,然后在需要重置到標記位置的時候,調(diào)用reset方法重置

看一下這兩個方法的內(nèi)部實現(xiàn)

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;
}

方法的內(nèi)部實現(xiàn)非常簡單,通過對變量mark的編輯來標記當前位置,在需要重置的時候,把變量mark的值賦給當前下標索引position來達到重置的目的。

clear

clear方法比較簡單,將position = 0,limit = capacity,mark = -1。有點類似初始化時的操作

compact

最后再來介紹一下compact方法,首先我們先來看一個場景

  while (in.read(buf) >= 0 || buf.position != 0) {
     buf.flip();
     out.write(buf);
     buf.clear();
 }

上面這個例子,在非阻塞模式下是會有問題的,因為write方法我們并不知道一次性可以寫多少個字節(jié),所以有可能還有未寫入的字節(jié)數(shù)據(jù),這時候我們卻又重新讀取了新的數(shù)據(jù),就會導(dǎo)致覆蓋了原有還未寫入的數(shù)據(jù)。

只需要改成以下的方式即可,該方法的作用是將positionlimit之間的數(shù)據(jù)復(fù)制到buffer的開始位置,復(fù)制后position = limit -position,limit = capacity

  while (in.read(buf) >= 0 || buf.position != 0) {
     buf.flip();
     out.write(buf);
     buf.compact();
 }

總結(jié)

本編文章比較詳細的分析了Java NIOBuffer的基本使用及內(nèi)部實現(xiàn)原理。Buffer的內(nèi)部實現(xiàn)其實比較簡單,需要著重理解的其實就是capacity position limit mark這四個變量屬性。

博客原文地址戳這里

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容