在軟體系統中,I/O的速度要比記憶體的速度慢很多,因此I/O經常會稱為系統的瓶頸。所有,提高I/O速度,對於提升系統的整體性能有很大的作用。 在java標準的I/O中,是基於流的I/O的實現,即InputStream和OutPutStream,這種基於流的實現以位元組為基本單元,很容易實現各種過濾器。
- 前言
在軟體系統中,I/O的速度要比記憶體的速度慢很多,因此I/O經常會稱為系統的瓶頸。所有,提高I/O速度,對於提升系統的整體性能有很大的作用。
在java標準的I/O中,是基於流的I/O的實現,即InputStream和OutPutStream,這種基於流的實現以位元組為基本單元,很容易實現各種過濾器。
NIO和new I/O的簡稱,在java1.4納入JDK中,具有以下特征:
1、為所有的原始類型提供(buffer)緩存支持;
2、使用Charset作為字元集編碼解碼解決方案;
3、增加了通道(Channel)對象,作為新的原始I/O抽象;
4、支持鎖和記憶體訪問文件的文件訪問介面;
5、提供了基於Selector的非同步網路I/O;
NIO是基於塊(Block)的,它以塊為基本單位處理數據。在NIO中,最重要的兩個組件是buffer緩衝和channel通道。緩衝是一塊連續的記憶體區域,是NIO讀寫數據的中轉站。通道表示緩衝數據的源頭或目的地,它用於向緩衝讀取或寫入數據,是訪問緩衝的介面。通道和緩衝的關係如圖:
- NIO中的Buffer類和Channel
JDK為每一種java原生類型都提供了一種Buffer,除了ByteBuffer外,其他每一種Buffer都具有完全一樣的操作,除了操作類型不一樣以外。ByteBuffer可以用於絕大多數標準I/O操作的介面。
在NIO中和Buffer配合使用的還有Channel。Channel是一個雙向通道,既可以讀也可以寫。有點類似Stream,但是Stream是單向的。應用程式不能直接對Channel進行讀寫操作,而必須通過Buffer來進行。
下麵以一個文件複製為例,簡單介紹NIO的Buffer和Channel的用法,代碼如下:
1 public class NioCopyFileTest { 2 public static void main(String[] args) throws Exception { 3 NioCopyFileTest.copy("test.txt", "test2.txt"); 4 } 5 6 public static void copy(String resource,String destination) throws Exception{ 7 FileInputStream fis = new FileInputStream(resource); 8 FileOutputStream fos = new FileOutputStream(destination); 9 10 FileChannel inputFileChannel = fis.getChannel();//讀文件通道 11 FileChannel outputFileChannel = fos.getChannel();//寫文件通道 12 ByteBuffer byteBuffer = ByteBuffer.allocate(1024);//讀寫數據緩衝 13 while(true){ 14 byteBuffer.clear(); 15 int length = inputFileChannel.read(byteBuffer);//讀取數據 16 if(length == -1){ 17 break;//讀取完畢 18 } 19 byteBuffer.flip(); 20 outputFileChannel.write(byteBuffer);//寫入數據 21 } 22 inputFileChannel.close(); 23 outputFileChannel.close(); 24 } 25 }
代碼中註釋寫的很詳細了,輸入流和輸出流都對應一個Channel通道,將數據通過讀文件channel讀取到緩衝中,然後再通過寫文件channel寫入到緩衝中。這樣就完成了文件複製。註意:緩衝在文件傳輸中起到的作用十分大,可以緩解記憶體和硬碟之間的性能差異,提升系統性能。
- Buffer的基本原理
Buffer有三個重要的參數:位置(position)、容量(capactiy)和上限(limit)。這三個參數的含義如下圖:
下麵例子很好的解釋了Buffer的工作原理:
1 ByteBuffer buffer = ByteBuffer.allocate(15);//設置緩衝區大小為15 2 System.out.println("position:"+buffer.position()+"limit:"+buffer.limit()+"capacity"+buffer.capacity()); 3 for (int i = 0; i < 10; i++) { 4 buffer.put((byte) i); 5 } 6 System.out.println("position:"+buffer.position()+"limit:"+buffer.limit()+"capacity"+buffer.capacity()); 7 buffer.flip();//重置position 8 for (int i = 0; i < 5; i++) { 9 System.out.println(buffer.get()); 10 } 11 System.out.println("position:"+buffer.position()+"limit:"+buffer.limit()+"capacity"+buffer.capacity()); 12 buffer.flip(); 13 System.out.println("position:"+buffer.position()+"limit:"+buffer.limit()+"capacity"+buffer.capacity());
以上代碼,先分配了15個位元組大小的緩衝區。在初始階段,position為0,capacity為15,limit為15。註意,position是從0開始的,所以索引為15的位置實際上是不存在的。
接著往緩衝區放入10個元素,position始終指向下一個即將放入的位置,所有position為10,capacity和limit依然為15。
進行flip()操作,會重置position的位置,並且將limit設置到當前position的位置,這時Buffer從寫模式進入讀模式,這樣就可以防止讀操作讀取到沒有進行操作的位置。所有此時,position為0,limit為10,capacity為15。
接著進行五次讀操作,讀操作會設置position的位置,所以,position為5,limit為10,capacity為15。
在進行一次flip()操作,此時可想而知position為0,limit為5,capacity為15。
- Buffer的相關操作
Buffer是NIO中最核心的對象,它的一系列的操作和使用也需要重點掌握,這裡簡單概括一下,也可以參考相關API查看。
1、Buffer的創建:
buffer的常見有兩種方式,使用靜態方法allocate()從堆中分配緩衝區,或者從一個既有數組中創建緩衝區。
1 ByteBuffer buffer = ByteBuffer.allocate(1024);//從堆中分配 2 byte[] arrays = new byte[1024];//從既有數組中創建 3 ByteBuffer buffer2 = ByteBuffer.wrap(arrays);
2、重置或清空緩衝區:
buffer還提供了一些用於重置和清空緩衝區的方法:rewind(),clear(),flip()。它們的作用如下:
3、讀寫緩衝區:
對Buffer對象進行讀寫操作是Buffer最重要的操作,buffer提供了許多讀寫操作的緩衝區。具體參考API。
4、標誌緩衝區
標誌(mark)緩衝區是一個在數據處理時很有用的功能,它就像書簽一樣,可以在數據處理中隨時記錄當前位置,然後再任意時刻回到這個位置,從而簡化或加快數據處理的流程。相關函數為:mark()和reset()。mark()用於記錄當前位置,reset()用於恢復到mark標記的位置。
代碼如下:
1 ByteBuffer buffer = ByteBuffer.allocate(15);//設置緩衝區大小為15 2 for (int i = 0; i < 10; i++) { 3 buffer.put((byte) i); 4 } 5 buffer.flip();//重置position 6 for (int i = 0; i < buffer.limit(); i++) { 7 System.out.print(buffer.get()); 8 if(i==4){ 9 buffer.mark(); 10 System.out.print("mark at"+i); 11 } 12 } 13 System.out.println(); 14 buffer.reset(); 15 while(buffer.hasRemaining()){ 16 System.out.print(buffer.get()); 17 }
輸出結果:
1 01234mark at456789 2 56789
5、複製緩衝區
複製緩衝區是以原緩衝區為基礎,生成一個完全一樣的緩衝區。方法為:duplicate()。這個函數對於處理複雜的Buffer數據很有好處。因為新生成的緩衝區和元緩衝區共用相同的記憶體數據。並且,任意一方的改動都是互相可見的,但是兩者又各自維護者自己的position、limit和capacity。這大大增加了程式的靈活性,為多方同時處理數據提供了可能。
代碼如下:
1 ByteBuffer buffer = ByteBuffer.allocate(15);//設置緩衝區大小為15 2 for (int i = 0; i < 10; i++) { 3 buffer.put((byte) i); 4 } 5 ByteBuffer buffer2 = buffer.duplicate();//複製當前緩衝區 6 System.out.println("after buffer duplicate"); 7 System.out.println(buffer); 8 System.out.println(buffer2); 9 buffer2.flip(); 10 System.out.println("after buffer2 flip"); 11 System.out.println(buffer); 12 System.out.println(buffer2); 13 buffer2.put((byte)100); 14 System.out.println("after buffer2 put"); 15 System.out.println(buffer.get(0)); 16 System.out.println(buffer2.get(0));
輸出結果如下:
1 after buffer duplicate 2 java.nio.HeapByteBuffer[pos=10 lim=15 cap=15] 3 java.nio.HeapByteBuffer[pos=10 lim=15 cap=15] 4 after buffer2 flip 5 java.nio.HeapByteBuffer[pos=10 lim=15 cap=15] 6 java.nio.HeapByteBuffer[pos=0 lim=10 cap=15] 7 after buffer2 put 8 100 9 100
6、緩衝區分片
緩衝區分片使用slice()方法,它將現有的緩衝區創建新的子緩衝區,子緩衝區和父緩衝區共用數據,子緩衝區具有完整的緩衝區模型結構。當處理一個buffer的一個片段時,可以使用一個slice()方法取得一個子緩衝區,然後就像處理普通緩衝區一樣處理這個子緩衝區,而無需考慮邊界問題,這樣有助於系統模塊化。
1 ByteBuffer buffer = ByteBuffer.allocate(15);//設置緩衝區大小為15 2 for (int i = 0; i < 10; i++) { 3 buffer.put((byte) i); 4 } 5 buffer.position(2); 6 buffer.limit(6); 7 ByteBuffer subBuffer = buffer.slice();//複製緩衝區 8 for (int i = 0; i < subBuffer.limit(); i++) { 9 byte b = subBuffer.get(i); 10 b=(byte) (b*10); 11 subBuffer.put(i, b); 12 } 13 buffer.limit(buffer.capacity()); 14 buffer.position(0); 15 for (int i = 0; i < buffer.limit(); i++) { 16 System.out.print(buffer.get(i)+" "); 17 }
輸出結果:
1 0 1 20 30 40 50 6 7 8 9 0 0 0 0 0
7、只讀緩衝區
可以使用緩衝區對象的asReadOnlyBuffer()方法得到一個與當前緩衝區一致的,並且共用記憶體數據的只讀緩衝區,只讀緩衝區對於數據安全非常有用。使用只讀緩衝區可以保證數據不被修改,同時,只讀緩衝區和原始緩衝區是共用記憶體塊的,因此,對於原始緩衝區的修改,只讀緩衝區也是可見的。
代碼如下:
1 ByteBuffer buffer = ByteBuffer.allocate(15);//設置緩衝區大小為15 2 for (int i = 0; i < 10; i++) { 3 buffer.put((byte) i); 4 } 5 ByteBuffer readBuffer = buffer.asReadOnlyBuffer(); 6 for (int i = 0; i < readBuffer.limit(); i++) { 7 System.out.print(readBuffer.get(i)+" "); 8 } 9 System.out.println(); 10 buffer.put(2, (byte)20); 11 for (int i = 0; i < readBuffer.limit(); i++) { 12 System.out.print(readBuffer.get(i)+" "); 13 }
結果:
1 0 1 2 3 4 5 6 7 8 9 0 0 0 0 0 2 0 1 20 3 4 5 6 7 8 9 0 0 0 0 0
由此可見,只讀緩衝區並不是原始緩衝區在某一時刻的快照,而是和原始緩衝區共用記憶體數據的。當修改只讀緩衝區時,會報ReadOnlyBufferException異常。
8、文件映射到記憶體:
NIO提供了一種將文件映射到記憶體的方法進行I/O操作,它可以比常規的基於流的I/O快很多。這個操作主要是由FileChannel.map()方法實現的。
使用文件映射的方式,將文本文件通過FileChannel映射到記憶體中。然後在記憶體中讀取文件內容。還可以修改Buffer,將實際數據寫到對應的硬碟中。
1 RandomAccessFile raf = new RandomAccessFile("D:\\test.txt", "rw"); 2 FileChannel fc = raf.getChannel(); 3 MappedByteBuffer mbf = fc.map(MapMode.READ_WRITE, 0, raf.length());//將文件映射到記憶體 4 while(mbf.hasRemaining()){ 5 System.out.println(mbf.get()); 6 } 7 mbf.put(0,(byte)98);//修改文件 8 raf.close();
9、處理結構化數據
NIO還提供了處理結構化數據的方法,稱為散射和聚集。散射是將一組數據讀入到一組buffer中,聚集是將數據寫入到一組buffer中。聚集和散射的基本使用方法和對單個buffer操作的使用方法類似。這一組緩衝區類似於一個大的緩衝區。
散射/聚集IO對處理結構化數據非常有用。例如,對於一個具有固定格式的文件的讀寫,在已知文件具體結構的情況下,可以構造若幹個符合文件結構的buffer,使得各個buffer的大小恰好符合文件各段結構的大小。
例如,將"姓名:張三,年齡:18",通過聚集寫創建該文件,然後再通過散射都來解析。
1 ByteBuffer nameBuffer = ByteBuffer.wrap("姓名:張三,".getBytes("utf-8")); 2 ByteBuffer ageBuffer = ByteBuffer.wrap("年齡:18".getBytes("utf-8")); 3 int nameLength = nameBuffer.limit(); 4 int ageLength = ageBuffer.limit(); 5 ByteBuffer[] bufs = new ByteBuffer[]{nameBuffer,ageBuffer}; 6 File file = new File("D:\\name.txt"); 7 if(!file.exists()){ 8 file.createNewFile(); 9 } 10 FileOutputStream fos = new FileOutputStream(file); 11 FileChannel channel = fos.getChannel(); 12 channel.write(bufs); 13 channel.close(); 14 15 ByteBuffer nameBuffer2 = ByteBuffer.allocate(nameLength); 16 ByteBuffer ageBuffer2 = ByteBuffer.allocate(ageLength); 17 ByteBuffer[] bufs2 = new ByteBuffer[]{nameBuffer2,ageBuffer2}; 18 FileInputStream fis = new FileInputStream("D:\\name.txt"); 19 FileChannel channel2 = fis.getChannel(); 20 channel2.read(bufs2); 21 String name = new String(bufs2[0].array(),"utf-8"); 22 String age = new String(bufs2[1].array(),"utf-8"); 23 24 System.out.println(name+age);
通過和通道的配合使用,可以簡化Buffer對於結構化數據處理的難度。
註意,ByteBuffer是將文件一次性讀入記憶體再做處理,而Stream方式則是邊讀取文件邊處理數據,這也是兩者性能差異的主要原因。
- 直接記憶體訪問
NIO的Buffer還提供了一個可以直接訪問系統物理記憶體的類--DirectBuffer。普通的ByteBuffer依然在JVM堆上分配空間,其最大記憶體,受最大堆的限制。而DirecBuffer直接分配在物理記憶體中,並不占用堆空間。創建DirectBuffer的方法是:ByteBuffer.allocateDirect(capacity)。
在對普通的ByteBuffer的訪問,系統總會使用一個"內核緩衝區"進行間接操作。而ByteBuffer所處的位置,就相當於這個"內核緩衝區"。因此,DirecBuffer是一種更加接近底層的操作。
DirectBuffer的訪問速度遠高於ByteBuffer,但是其創建和銷毀所消耗的時間卻遠大於ByteBuffer。在需要頻繁創建和銷毀Buffer的場合,顯然不適合DirectBuffer的使用,但是如果能將DirectBuffer進行復用,那麼在讀寫頻繁的場合下,它完全可以大幅度改善系統性能。