引言
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類型可供使用
- ByteBuffer
- MappedByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- 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)原理進行分析,在分析之前,我們有必要先認識一下Buffer的capacity 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

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

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

四. 現(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,如下圖顯示

五. 成功翻轉(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ù)。
只需要改成以下的方式即可,該方法的作用是將position與limit之間的數(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這四個變量屬性。
