Java NIO主要解決了Java IO的效率問題,解決此問題的思路之一是利用硬件和操作系統(tǒng)直接支持的緩沖區(qū)染乌、虛擬內(nèi)存俺榆、磁盤控制器直接讀寫等優(yōu)化IO的手段;思路之二是提供新的編程架構(gòu)使得單個線程可以控制多個IO粒蜈,從而節(jié)約線程資源顺献,提高IO性能。
Java IO引入了三個主要概念枯怖,即緩沖區(qū)(Buffer)注整、通道(Channel)和選擇器(Selector),本文主要介紹緩沖區(qū)度硝。
- 緩沖區(qū)概念
緩沖區(qū)是對Java原生數(shù)組的對象封裝肿轨,它除了包含其數(shù)組外,還帶有四個描述緩沖區(qū)特征的屬性以及一組用來操作緩沖區(qū)的API蕊程。緩沖區(qū)的根類是Buffer椒袍,其重要的子類包括ByteBuffer、MappedByteBuffer藻茂、CharBuffer驹暑、IntBuffer、DoubleBuffer辨赐、ShortBuffer优俘、LongBuffer、FloatBuffer掀序。從其名稱可以看出這些類分別對應了存儲不同類型數(shù)據(jù)的緩沖區(qū)帆焕。
1.1四個屬性
緩沖區(qū)由四個屬性指明其狀態(tài)。
容量(Capacity):緩沖區(qū)能夠容納的數(shù)據(jù)元素的最大數(shù)量森枪。初始設定后不能更改视搏。
上界(Limit):緩沖區(qū)中第一個不能被讀或者寫的元素位置审孽。或者說浑娜,緩沖區(qū)內(nèi)現(xiàn)存元素的上界佑力。
位置(Position):緩沖區(qū)內(nèi)下一個將要被讀或?qū)懙脑匚恢谩T谶M行讀寫緩沖區(qū)時筋遭,位置會自動更新打颤。
標記(Mark):一個備忘位置。初始時為“未定義”漓滔,調(diào)用mark時mark=positon编饺,調(diào)用reset時position=mark。
這四個屬性總是滿足如下關(guān)系:
mark<=position<=limit<=capacity
如果我們創(chuàng)建一個新的容量大小為10的ByteBuffer對象如下圖所示:
在初始化的時候响驴,position設置為0透且,limit和 capacity被設置為10,在以后使用ByteBuffer對象過程中豁鲤,capacity的值不會再發(fā)生變化秽誊,而其它兩個個將會隨著使用而變化。三個屬性值分別如圖所示:
現(xiàn)在我們可以從通道中讀取一些數(shù)據(jù)到緩沖區(qū)中琳骡,注意從通道讀取數(shù)據(jù)锅论,相當于往緩沖區(qū)中寫入數(shù)據(jù)。如果讀取4個字節(jié)的數(shù)據(jù)楣号,則此時position的值為4最易,即下一個將要被寫入的字節(jié)索引為4,而limit仍然是10炫狱,如下圖所示:
下一步把讀取的數(shù)據(jù)寫入到輸出通道中藻懒,相當于從緩沖區(qū)中讀取數(shù)據(jù),在此之前视译,必須調(diào)用flip()方法束析,該方法將會完成兩件事情:
- 把limit設置為當前的position值
-
把position設置為0
由于position被設置為0,所以可以保證在下一步輸出時讀取到的是緩沖區(qū)中的第一個字節(jié)憎亚,而limit被設置為當前的position员寇,可以保證讀取的數(shù)據(jù)正好是之前寫入到緩沖區(qū)中的數(shù)據(jù),如下圖所示:
image.png
現(xiàn)在調(diào)用get()方法從緩沖區(qū)中讀取數(shù)據(jù)寫入到輸出通道第美,這會導致position的增加而limit保持不變蝶锋,但position不會超過limit的值,所以在讀取我們之前寫入到緩沖區(qū)中的4個自己之后什往,position和limit的值都為4扳缕,如下圖所示:
在從緩沖區(qū)中讀取數(shù)據(jù)完畢后,limit的值仍然保持在我們調(diào)用flip()方法時的值,調(diào)用clear()方法能夠把所有的狀態(tài)變化設置為初始化時的值躯舔,如下圖所示:
下面這個例子可以展示buffer的讀寫:
public class NioTest1 {
public static void main(String[] args) {
//通過nio生成隨機數(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);
//這里相當于把數(shù)據(jù)寫到buffer中
buffer.put(randomNumber);
}
System.out.println("before flip limit:"+buffer.capacity());
//上面是寫,下面為讀粥庄,通過flip()方法進行讀寫的切換
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());
//這里相當于從buffer中讀出數(shù)據(jù)
System.out.println(buffer.get());
}
}
1.3 remaining和hasRemaining
remaining()會返回緩沖區(qū)中目前存儲的元素個數(shù)丧失,在使用參數(shù)為數(shù)組的get方法中,提前知道緩沖區(qū)存儲的元素個數(shù)是非常有用的惜互。
事實上布讹,由于緩沖區(qū)的讀或者寫模式并不清晰,因此實際上remaining()返回的僅僅是limit – position的值训堆。
而hasRemaining()的含義是查詢緩沖區(qū)中是否還有元素描验,這個方法的好處是它是線程安全的。
1.4 Flip翻轉(zhuǎn)
在從緩沖區(qū)中讀取數(shù)據(jù)時坑鱼,get方法會從position的位置開始膘流,依次讀取數(shù)據(jù),每次讀取后position會自動加1鲁沥,直至position到達limit處為止睡扬。因此,在寫入數(shù)據(jù)后黍析,開始讀數(shù)據(jù)前,需要設置position和limit的值屎开,以便get方法能夠正確讀入前面寫入的元素阐枣。
這個設置應該是讓limit=position,然后position=0奄抽,為了方便蔼两,Buffer類提供了一個方法flip(),來完成這個設置逞度。其代碼如下:
/**
* 測試flip操作额划,flip就是從寫入轉(zhuǎn)為讀出前的一個設置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俊戳,其剩下的部分整體挪動到buffer的頭部(即從0開始的一段位置),便于后續(xù)的寫入或者讀取馆匿。其含義為limit=limit-position抑胎,position=0,測試代碼如下:
private static void testCompact() {
CharBuffer buffer = CharBuffer.allocate(10);
buffer.put("abcde");
buffer.flip();
//先讀取兩個字符
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復制
復制緩沖區(qū)渐北,兩個緩沖區(qū)對象實際上指向了同一個內(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ū)切片,將一個大緩沖區(qū)的一部分切出來恃锉,作為一個單獨的緩沖區(qū)搀菩,但是它們公用同一個內(nèi)部數(shù)組。切片從原緩沖區(qū)的position位置開始破托,至limit為止肪跋。原緩沖區(qū)和切片各自擁有自己的屬性,測試代碼如下:
/**
* 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
我們可以隨時將一個普通Buffer調(diào)用asReadOnlyBuffer方法返回一個只讀Buffer炼团,但不能將一個只讀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ū)瘟芝,但實際上應用最廣易桃,使用頻率最高,也是最重要的緩沖區(qū)是字節(jié)緩沖區(qū)ByteBuffer锌俱。因為ByteBuffer中直接存儲字節(jié)晤郑,所以在不同的操作系統(tǒng)、硬件平臺贸宏、文件系統(tǒng)和JDK之間傳遞數(shù)據(jù)時不涉及編碼造寝、解碼和亂碼問題,也不涉及Big-Endian和Little-Endian大小端問題吭练,所以它是使用最為便利的一種緩沖區(qū)诫龙。
2.1視圖緩沖區(qū)
ByteBuffer中存儲的是字節(jié),有時為了方便鲫咽,可以使用asCharBuffer()等方法將ByteBuffer轉(zhuǎn)換為存儲某基本類型的視圖签赃,例如CharBuffer、IntBuffer分尸、DoubleBuffer锦聊、ShortBuffer、LongBuffer和FloatBuffer箩绍。
如此轉(zhuǎn)換后孔庭,這兩個緩沖區(qū)共享同一個內(nèi)部數(shù)組,但是對數(shù)組內(nèi)元素的視角不同材蛛。以CharBuffer和ByteBuffer為例圆到,ByteBuffer將其視為一個個的字節(jié)(1個字節(jié)),而CharBuffer則將其視為一個個的字符(2個字節(jié))卑吭。若此ByteBuffer的capacity為12构资,則對應的CharBuffer的capacity為12/2=6。與duplicate創(chuàng)建的復制緩沖區(qū)類似陨簇,該CharBuffer和ByteBuffer也各自管理自己的緩沖區(qū)屬性吐绵。
還有一點需要注意的是迹淌,在創(chuàng)建視圖緩沖區(qū)的時候ByteBuffer的position屬性的取值很重要,視圖會以當前position的值為開頭己单,以limit為結(jié)尾唉窃。例子如下:
private static void testElementView() {
ByteBuffer buffer =ByteBuffer.allocate(12);
//存入四個字節(jié),0x00000042
buffer.put((byte) 0x00).put((byte)0x00).put((byte) 0x00).put((byte) 0x42);
buffer.position(0);
//轉(zhuǎn)換為IntBuffer,并取出一個int(四個字節(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);
//直接存入一個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é)序了兄猩,詳細參見https://zhuanlan.zhihu.com/p/25435644茉盏。
簡單說來,當某個元素(char枢冤、int鸠姨、double)的長度超過了1個字節(jié)時,則由于種種歷史原因淹真,它在內(nèi)存中的存儲方式有兩種讶迁,一種是Big-Endian,一種是Little-Endian核蘸。
Big-Endian就是高位字節(jié)排放在內(nèi)存的低地址端巍糯,低位字節(jié)排放在內(nèi)存的高地址端。 簡單來說值纱,就是我們?nèi)祟愂煜さ拇娣欧绞健?br>
Little-Endian就是低位字節(jié)排放在內(nèi)存的低地址端,高位字節(jié)排放在內(nèi)存的高地址端坯汤。
Java默認是使用Big-Endian的虐唠,因此上面的代碼都是以這種方式來存放元素的。但是惰聂,其他的一些硬件(CPU)疆偿、操作系統(tǒng)或者語言可能是以Little-Endian的方式來存儲元素的。因此NIO提供了相應的API來支持緩沖區(qū)設置為不同的字節(jié)序搓幌,其方法很簡單杆故,代碼如下:
privatestatic void testByteOrder() {
ByteBuffer buffer =ByteBuffer.allocate(12);
//直接存入一個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方法可以隨時設置buffer的字節(jié)序,其參數(shù)取值為ByteOrder.LITTLE_ENDIAN以及ByteOrder.BIG_ENDIAN溉愁。
2.4直接緩沖區(qū) DirectByteBuffer
最后一個需要掌握的概念是直接緩沖區(qū)处铛,它是以創(chuàng)建時的開銷換取了IO時的高效率。另外一點是,直接緩沖區(qū)使用的內(nèi)存是直接調(diào)用了操作系統(tǒng)api分配的撤蟆,繞過了JVM堆棧奕塑。
直接緩沖區(qū)通過ByteBuffer.allocateDirect()方法創(chuàng)建,并可以調(diào)用isDirect()來查詢一個緩沖區(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)存中任何的信息修改,最終都會被寫入到磁盤文件中反镇,即MappedByteBuffer是一種允許java程序直接從內(nèi)存訪問的特殊的文件固蚤,可以將整個文件或整個文件的一部分映射到內(nèi)存中,由操作系統(tǒng)負責將頁面請求的內(nèi)存數(shù)據(jù)修改寫入到文件中愿险。應用程序只需要處理內(nèi)存的數(shù)據(jù)颇蜡,這樣可以實現(xiàn)迅速的IO操作,用于內(nèi)存映射文件的這個內(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引入了更多的概念和復雜性风秤,這一切的努力都是為了實現(xiàn)NIO的經(jīng)典編程模式,即用一個線程來控制多路IO扮叨,從而極大的提高服務器端IO效率缤弦。Buffer、Channel和Selector共同實現(xiàn)了NIO的編程模式彻磁,其中Buffer也可以被獨立的使用碍沐,用來完成緩沖區(qū)的功能。
Java NIO通俗編程之緩沖區(qū)內(nèi)部細節(jié)狀態(tài)變量position,limit,capacity(二)