Java NIO主要解決了Java IO的效率問題,解決此問題的思路之一是利用硬件和操作系統(tǒng)直接支持的緩沖區(qū)、虛擬內(nèi)存、磁盤控制器直接讀寫等優(yōu)化IO的手段;思路之二是提供新的編程架構(gòu)使得單個(gè)線程可以控制多個(gè)IO,從而節(jié)約線程資源,提高IO性能。
Java IO引入了三個(gè)主要概念,即緩沖區(qū)(Buffer)、通道(Channel)和選擇器(Selector),本文主要介紹緩沖區(qū)。
- 緩沖區(qū)概念
緩沖區(qū)是對(duì)Java原生數(shù)組的對(duì)象封裝,它除了包含其數(shù)組外,還帶有四個(gè)描述緩沖區(qū)特征的屬性以及一組用來操作緩沖區(qū)的API。緩沖區(qū)的根類是Buffer,其重要的子類包括ByteBuffer、MappedByteBuffer、CharBuffer、IntBuffer、DoubleBuffer、ShortBuffer、LongBuffer、FloatBuffer。從其名稱可以看出這些類分別對(duì)應(yīng)了存儲(chǔ)不同類型數(shù)據(jù)的緩沖區(qū)。
1.1四個(gè)屬性
緩沖區(qū)由四個(gè)屬性指明其狀態(tài)。
容量(Capacity):緩沖區(qū)能夠容納的數(shù)據(jù)元素的最大數(shù)量。初始設(shè)定后不能更改。
上界(Limit):緩沖區(qū)中第一個(gè)不能被讀或者寫的元素位置?;蛘哒f,緩沖區(qū)內(nèi)現(xiàn)存元素的上界。
位置(Position):緩沖區(qū)內(nèi)下一個(gè)將要被讀或?qū)懙脑匚恢?。在進(jìn)行讀寫緩沖區(qū)時(shí),位置會(huì)自動(dòng)更新。
標(biāo)記(Mark):一個(gè)備忘位置。初始時(shí)為“未定義”,調(diào)用mark時(shí)mark=positon,調(diào)用reset時(shí)position=mark。
這四個(gè)屬性總是滿足如下關(guān)系:
mark<=position<=limit<=capacity
如果我們創(chuàng)建一個(gè)新的容量大小為10的ByteBuffer對(duì)象如下圖所示:

在初始化的時(shí)候,position設(shè)置為0,limit和 capacity被設(shè)置為10,在以后使用ByteBuffer對(duì)象過程中,capacity的值不會(huì)再發(fā)生變化,而其它兩個(gè)個(gè)將會(huì)隨著使用而變化。三個(gè)屬性值分別如圖所示:

現(xiàn)在我們可以從通道中讀取一些數(shù)據(jù)到緩沖區(qū)中,注意從通道讀取數(shù)據(jù),相當(dāng)于往緩沖區(qū)中寫入數(shù)據(jù)。如果讀取4個(gè)字節(jié)的數(shù)據(jù),則此時(shí)position的值為4,即下一個(gè)將要被寫入的字節(jié)索引為4,而limit仍然是10,如下圖所示:

下一步把讀取的數(shù)據(jù)寫入到輸出通道中,相當(dāng)于從緩沖區(qū)中讀取數(shù)據(jù),在此之前,必須調(diào)用flip()方法,該方法將會(huì)完成兩件事情:
- 把limit設(shè)置為當(dāng)前的position值
-
把position設(shè)置為0
由于position被設(shè)置為0,所以可以保證在下一步輸出時(shí)讀取到的是緩沖區(qū)中的第一個(gè)字節(jié),而limit被設(shè)置為當(dāng)前的position,可以保證讀取的數(shù)據(jù)正好是之前寫入到緩沖區(qū)中的數(shù)據(jù),如下圖所示:
image.png
現(xiàn)在調(diào)用get()方法從緩沖區(qū)中讀取數(shù)據(jù)寫入到輸出通道,這會(huì)導(dǎo)致position的增加而limit保持不變,但position不會(huì)超過limit的值,所以在讀取我們之前寫入到緩沖區(qū)中的4個(gè)自己之后,position和limit的值都為4,如下圖所示:

在從緩沖區(qū)中讀取數(shù)據(jù)完畢后,limit的值仍然保持在我們調(diào)用flip()方法時(shí)的值,調(diào)用clear()方法能夠把所有的狀態(tài)變化設(shè)置為初始化時(shí)的值,如下圖所示:

下面這個(gè)例子可以展示buffer的讀寫:
public class NioTest1 {
public static void main(String[] args) {
//通過nio生成隨機(jī)數(shù),然后在打印出來
IntBuffer buffer = IntBuffer.allocate(10);
System.out.println("capacity:"+buffer.capacity());
for (int i = 0;i < 5;i++){
int randomNumber = new SecureRandom().nextInt(20);
//這里相當(dāng)于把數(shù)據(jù)寫到buffer中
buffer.put(randomNumber);
}
System.out.println("before flip limit:"+buffer.capacity());
//上面是寫,下面為讀,通過flip()方法進(jìn)行讀寫的切換
buffer.flip();
System.out.println("after flip limit:"+buffer.capacity());
System.out.println("enter while loop");
while(buffer.hasRemaining()){
System.out.println("position:" + buffer.position());
System.out.println("limit:" + buffer.limit());
System.out.println("capacity:" + buffer.capacity());
//這里相當(dāng)于從buffer中讀出數(shù)據(jù)
System.out.println(buffer.get());
}
}
1.3 remaining和hasRemaining
remaining()會(huì)返回緩沖區(qū)中目前存儲(chǔ)的元素個(gè)數(shù),在使用參數(shù)為數(shù)組的get方法中,提前知道緩沖區(qū)存儲(chǔ)的元素個(gè)數(shù)是非常有用的。
事實(shí)上,由于緩沖區(qū)的讀或者寫模式并不清晰,因此實(shí)際上remaining()返回的僅僅是limit – position的值。
而hasRemaining()的含義是查詢緩沖區(qū)中是否還有元素,這個(gè)方法的好處是它是線程安全的。
1.4 Flip翻轉(zhuǎn)
在從緩沖區(qū)中讀取數(shù)據(jù)時(shí),get方法會(huì)從position的位置開始,依次讀取數(shù)據(jù),每次讀取后position會(huì)自動(dòng)加1,直至position到達(dá)limit處為止。因此,在寫入數(shù)據(jù)后,開始讀數(shù)據(jù)前,需要設(shè)置position和limit的值,以便get方法能夠正確讀入前面寫入的元素。
這個(gè)設(shè)置應(yīng)該是讓limit=position,然后position=0,為了方便,Buffer類提供了一個(gè)方法flip(),來完成這個(gè)設(shè)置。其代碼如下:
/**
* 測(cè)試flip操作,flip就是從寫入轉(zhuǎn)為讀出前的一個(gè)設(shè)置buffer屬性的操作,其意義是將limit=position,position=0
*/
private static void testFlip() {
CharBuffer buffer = CharBuffer.allocate(10);
buffer.put("abc");
buffer.flip();
char[] chars = new char[buffer.remaining()];
buffer.get(chars);
System.out.println(chars);
//以下操作與flip等同
buffer.clear();
buffer.put("abc");
buffer.limit(buffer.position());
buffer.position(0);
chars = new char[buffer.remaining()];
buffer.get(chars);
System.out.println(chars);
}
1.5compact壓縮
壓縮compact()方法是為了將讀取了一部分的buffer,其剩下的部分整體挪動(dòng)到buffer的頭部(即從0開始的一段位置),便于后續(xù)的寫入或者讀取。其含義為limit=limit-position,position=0,測(cè)試代碼如下:
private static void testCompact() {
CharBuffer buffer = CharBuffer.allocate(10);
buffer.put("abcde");
buffer.flip();
//先讀取兩個(gè)字符
buffer.get();
buffer.get();
showBuffer(buffer);
//壓縮
buffer.compact();
//繼續(xù)寫入
buffer.put("fghi");
buffer.flip();
showBuffer(buffer);
//從頭讀取后續(xù)的字符
char[] chars = new char[buffer.remaining()];
buffer.get(chars);
System.out.println(chars);
}
1.6duplicate復(fù)制
復(fù)制緩沖區(qū),兩個(gè)緩沖區(qū)對(duì)象實(shí)際上指向了同一個(gè)內(nèi)部數(shù)組,但分別管理各自的屬性。
private static void testDuplicate() {
CharBuffer buffer = CharBuffer.allocate(10);
buffer.put("abcde");
CharBuffer buffer1 = buffer.duplicate();
buffer1.clear();
buffer1.put("alex");
showBuffer(buffer);
showBuffer(buffer1);
}
1.7 slice緩沖區(qū)切片
緩沖區(qū)切片,將一個(gè)大緩沖區(qū)的一部分切出來,作為一個(gè)單獨(dú)的緩沖區(qū),但是它們公用同一個(gè)內(nèi)部數(shù)組。切片從原緩沖區(qū)的position位置開始,至limit為止。原緩沖區(qū)和切片各自擁有自己的屬性,測(cè)試代碼如下:
/**
* slice Buffer 和原有Buffer共享相同的底層數(shù)組
*/
public class NioTest6 {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10);
for (int i=0;i<buffer.capacity();i++){
buffer.put((byte)i);
}
buffer.position(2);
buffer.limit(6);
ByteBuffer sliceBuffer = buffer.slice();
for (int i=0;i < sliceBuffer.capacity();i++){
byte b = sliceBuffer.get(i);
b *= 2;
sliceBuffer.put(i,b);
}
buffer.position(0);
buffer.limit(buffer.capacity());
while(buffer.hasRemaining()){
System.out.println(buffer.get());
}
}
}
1.8只讀Buffer
我們可以隨時(shí)將一個(gè)普通Buffer調(diào)用asReadOnlyBuffer方法返回一個(gè)只讀Buffer,但不能將一個(gè)只讀Buffer轉(zhuǎn)換成讀寫B(tài)uffer
public class NioTest7 {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10);
System.out.println(buffer.getClass());
for (int i=0;i<buffer.capacity();i++){
buffer.put((byte)i);
}
ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
System.out.println(readOnlyBuffer.getClass());
//只讀buffer,不可以寫
// readOnlyBuffer.position(0);
// readOnlyBuffer.put((byte)2);
}
}
- 字節(jié)緩沖區(qū)
為了便于示例,前面的例子都使用了CharBuffer緩沖區(qū),但實(shí)際上應(yīng)用最廣,使用頻率最高,也是最重要的緩沖區(qū)是字節(jié)緩沖區(qū)ByteBuffer。因?yàn)锽yteBuffer中直接存儲(chǔ)字節(jié),所以在不同的操作系統(tǒng)、硬件平臺(tái)、文件系統(tǒng)和JDK之間傳遞數(shù)據(jù)時(shí)不涉及編碼、解碼和亂碼問題,也不涉及Big-Endian和Little-Endian大小端問題,所以它是使用最為便利的一種緩沖區(qū)。
2.1視圖緩沖區(qū)
ByteBuffer中存儲(chǔ)的是字節(jié),有時(shí)為了方便,可以使用asCharBuffer()等方法將ByteBuffer轉(zhuǎn)換為存儲(chǔ)某基本類型的視圖,例如CharBuffer、IntBuffer、DoubleBuffer、ShortBuffer、LongBuffer和FloatBuffer。
如此轉(zhuǎn)換后,這兩個(gè)緩沖區(qū)共享同一個(gè)內(nèi)部數(shù)組,但是對(duì)數(shù)組內(nèi)元素的視角不同。以CharBuffer和ByteBuffer為例,ByteBuffer將其視為一個(gè)個(gè)的字節(jié)(1個(gè)字節(jié)),而CharBuffer則將其視為一個(gè)個(gè)的字符(2個(gè)字節(jié))。若此ByteBuffer的capacity為12,則對(duì)應(yīng)的CharBuffer的capacity為12/2=6。與duplicate創(chuàng)建的復(fù)制緩沖區(qū)類似,該CharBuffer和ByteBuffer也各自管理自己的緩沖區(qū)屬性。
還有一點(diǎn)需要注意的是,在創(chuàng)建視圖緩沖區(qū)的時(shí)候ByteBuffer的position屬性的取值很重要,視圖會(huì)以當(dāng)前position的值為開頭,以limit為結(jié)尾。例子如下:
private static void testElementView() {
ByteBuffer buffer =ByteBuffer.allocate(12);
//存入四個(gè)字節(jié),0x00000042
buffer.put((byte) 0x00).put((byte)0x00).put((byte) 0x00).put((byte) 0x42);
buffer.position(0);
//轉(zhuǎn)換為IntBuffer,并取出一個(gè)int(四個(gè)字節(jié))
IntBuffer intBuffer =buffer.asIntBuffer();
int i =intBuffer.get();
System.out.println(Integer.toHexString(i));
}
不同元素需要的字節(jié)數(shù)不同:char為2字節(jié),short為2字節(jié),int為4字節(jié),float為4字節(jié),long為8字節(jié),double也是8字節(jié)。
2.2存取數(shù)據(jù)元素
也可以不通過視圖緩沖區(qū),直接向ByteBuffer中存入和取出不同類型的元素,其方法名為putChar()或者getChar()之類。例子如下:
private static void testPutAndGetElement() {
ByteBuffer buffer =ByteBuffer.allocate(12);
//直接存入一個(gè)int
buffer.putInt(0x1234abcd);
//以byte分別取出
buffer.position(0);
byte b1 = buffer.get();
byte b2 = buffer.get();
byte b3 = buffer.get();
byte b4 = buffer.get();
System.out.println(Integer.toHexString(b1&0xff));
System.out.println(Integer.toHexString(b2&0xff));
System.out.println(Integer.toHexString(b3&0xff));
System.out.println(Integer.toHexString(b4&0xff));
}
2.3 字節(jié)序
終于又要講到字節(jié)序了,詳細(xì)參見https://zhuanlan.zhihu.com/p/25435644。
簡(jiǎn)單說來,當(dāng)某個(gè)元素(char、int、double)的長(zhǎng)度超過了1個(gè)字節(jié)時(shí),則由于種種歷史原因,它在內(nèi)存中的存儲(chǔ)方式有兩種,一種是Big-Endian,一種是Little-Endian。
Big-Endian就是高位字節(jié)排放在內(nèi)存的低地址端,低位字節(jié)排放在內(nèi)存的高地址端。 簡(jiǎn)單來說,就是我們?nèi)祟愂煜さ拇娣欧绞健?br>
Little-Endian就是低位字節(jié)排放在內(nèi)存的低地址端,高位字節(jié)排放在內(nèi)存的高地址端。
Java默認(rèn)是使用Big-Endian的,因此上面的代碼都是以這種方式來存放元素的。但是,其他的一些硬件(CPU)、操作系統(tǒng)或者語言可能是以Little-Endian的方式來存儲(chǔ)元素的。因此NIO提供了相應(yīng)的API來支持緩沖區(qū)設(shè)置為不同的字節(jié)序,其方法很簡(jiǎn)單,代碼如下:
privatestatic void testByteOrder() {
ByteBuffer buffer =ByteBuffer.allocate(12);
//直接存入一個(gè)int
buffer.putInt(0x1234abcd);
buffer.position(0);
intbig_endian= buffer.getInt();
System.out.println(Integer.toHexString(big_endian));
buffer.rewind();
intlittle_endian=buffer.order(ByteOrder.LITTLE_ENDIAN).getInt();
System.out.println(Integer.toHexString(little_endian));
}
輸出為:
1234abcd
cdab3412
使用order方法可以隨時(shí)設(shè)置buffer的字節(jié)序,其參數(shù)取值為ByteOrder.LITTLE_ENDIAN以及ByteOrder.BIG_ENDIAN。
2.4直接緩沖區(qū) DirectByteBuffer
最后一個(gè)需要掌握的概念是直接緩沖區(qū),它是以創(chuàng)建時(shí)的開銷換取了IO時(shí)的高效率。另外一點(diǎn)是,直接緩沖區(qū)使用的內(nèi)存是直接調(diào)用了操作系統(tǒng)api分配的,繞過了JVM堆棧。
直接緩沖區(qū)通過ByteBuffer.allocateDirect()方法創(chuàng)建,并可以調(diào)用isDirect()來查詢一個(gè)緩沖區(qū)是否為直接緩沖區(qū)。
一般來說,直接緩沖區(qū)是最好的IO選擇。
public class NioTest8 {
public static void main(String[] args) throws IOException {
FileInputStream inputStream = new FileInputStream("input2.txt");
FileOutputStream outputStream = new FileOutputStream("output2.txt");
FileChannel inputChannel = inputStream.getChannel();
FileChannel outputChannel = outputStream.getChannel();
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while(true){
//每一次讀取之前都將buffer狀態(tài)初始化
buffer.clear();
int read = inputChannel.read(buffer);
System.out.println("read:" + read);
if(-1 == read){
break;
}
buffer.flip();
outputChannel.write(buffer);
}
inputChannel.close();
outputChannel.close();
}
}
2.5、MappedByteBuffer 內(nèi)存映射文件
文件的內(nèi)容直接映射到內(nèi)存里面,在內(nèi)存中任何的信息修改,最終都會(huì)被寫入到磁盤文件中,即MappedByteBuffer是一種允許java程序直接從內(nèi)存訪問的特殊的文件,可以將整個(gè)文件或整個(gè)文件的一部分映射到內(nèi)存中,由操作系統(tǒng)負(fù)責(zé)將頁面請(qǐng)求的內(nèi)存數(shù)據(jù)修改寫入到文件中。應(yīng)用程序只需要處理內(nèi)存的數(shù)據(jù),這樣可以實(shí)現(xiàn)迅速的IO操作,用于內(nèi)存映射文件的這個(gè)內(nèi)存是堆外內(nèi)存
public class NioTest9 {
public static void main(String[] args) throws IOException {
RandomAccessFile randomAccessFile = new RandomAccessFile("NioTest9.txt","rw");
FileChannel fileChannel = randomAccessFile.getChannel();
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 6);
mappedByteBuffer.put(0, (byte) 'a');
mappedByteBuffer.put(4, (byte) 'b');
randomAccessFile.close();
}
}
- 小結(jié)
與Stream相比,Buffer引入了更多的概念和復(fù)雜性,這一切的努力都是為了實(shí)現(xiàn)NIO的經(jīng)典編程模式,即用一個(gè)線程來控制多路IO,從而極大的提高服務(wù)器端IO效率。Buffer、Channel和Selector共同實(shí)現(xiàn)了NIO的編程模式,其中Buffer也可以被獨(dú)立的使用,用來完成緩沖區(qū)的功能。
Java NIO通俗編程之緩沖區(qū)內(nèi)部細(xì)節(jié)狀態(tài)變量position,limit,capacity(二)
