NIO是New I/O的簡稱,與舊式的基於流的I/O方法相對,從名字看,它表示新的一套Java I/O標准。
具有以下特性:
傳統Java IO,它是阻塞的,低效的。那么Java NIO和傳統Java IO有什么不同?帶來了什么?
(1)面向塊的I/O
傳統JavaIO是面向流的I/O。流I/O一次處理一個字節。NIO則是面向塊的I/O,每次操作都是以數據塊為單位。它們的差距就好象兩個人吃飯,一個人一粒一粒的吃,另一個人狼吞虎咽,快慢顯而易見。
NIO中引入了緩沖區(Buffer)的概念,緩沖區作為傳輸數據的基本單位塊,所有對數據的操作都是基於將數據移進/移出緩沖區而來;讀數據的時候從緩沖區中取,寫的時候將數據填入緩沖區。盡管傳統JavaIO中也有相應的緩沖區過濾器流(BufferedInputStream等),但是移進/移出的操作是由程序員來包裝的,它本質是對數據結構化和積累達到處理時的方便,並不是一種提高I/O效率的措施。NIO的緩沖區則不然,對緩沖區的移進/移出操作是由底層操作系統來實現的。
通常一次緩沖區操作是這樣的:某個進程需要進行I/O操作,它執行了一次讀(read)或者寫(write)的系統調用,向底層操作系統發出了請求,操作系統會按要求把數據緩沖區填滿或者排干。說起來簡單,其實很復雜。但至少我們知道了這事是由操作系統干的,比我們代碼級的實現要高效的多。
除了效率上的差別外,緩沖區在數據分析和處理上也帶來的很大的便利和靈活性。
(2)非阻塞的I/O + 就緒性選擇
傳統JavaIO是基於阻塞I/O模型的:當發起一個I/O請求時,如果數據沒有准備好(read時無可讀數據,write時數據不可寫入),那么線程便會阻塞,直到數據准備好,導致線程大部分的時間都在阻塞。
而非阻塞I/O則允許線程在有數據的時候處理數據,沒有數據的時候干點別的,提高了資源利用率。
就緒性選擇通常是建立在非阻塞的基礎上,並且更進一步,它把檢查哪些I/O請求的數據准備好這個任務交給了底層操作系統,操作系統會去查看並返回結果集合,這樣我們只需要關心那些准備好進行操作的IO通道。關於就緒性選擇的過程會在后面詳述。
NIO提供的Socket可以用非阻塞的方式工作,並且支持就緒性選擇,減少了資源消耗和CPU在線程間的切換,在管理線程效率上比傳統Socket高。
(3)文件鎖定和內存映射文件等操作系統特性
NIO同時帶來了很多當今操作系統大都支持的特性。
文件鎖定是多個進程協同工作的情況下,要協調進程間對共享數據的訪問必不可少的工具。
內存映射利用虛擬內存技術提供對文件的高速緩存,使讀取磁盤文件就像從內存中讀取一樣高效,但是卻不會有內存泄漏的危險,因為在內存中不會存在文件的完整拷貝。
此外還有一些其他的特性,后面再詳述。
(4)為所有的原始類型提供(Buffer)緩存支持
(5)使用Java.nio.charset.Charset作為字符集編碼解碼解決方案
(6)增加通道(Channel)對象,作為新的原始I/O抽象
(7)提供了基於Selector的異步網絡I/O。
為什么要使用NIO?
對於文件I/O, 在我看來使用IO和NIO是區別不大的,Java1.4開始原始IO也根據NIO重新實現過了,提供了對於NIO特性的支持。即使是流,也會比以前更加高效。企業級應用軟件中涉及I/O的部分多半是讀寫文件的功能性需求,很少有在並發上的要求,那么JavaIO包已經很勝任了。
對於網絡I/O,傳統的阻塞式I/O,一個線程對應一個連接,采用線程池的模式在大部分場景下簡單高效。當連接數茫茫多時,並且數據的移動非常頻繁,NIO無疑是更好的選擇。
NIO標榜的是高速、可伸縮的I/O,因為它更親近操作系統。當需求很平凡,沒有太高的效率要求的時候,你看不出它的好,反而覺得NIO代碼實現復雜,不易理解。選擇與否全看使用的場景,這點就看使用者的權衡了。
NIO的Buffer類族和Channel
在NIO中和Buffer配合使用的還有Channel。Channel是一個雙向通道,即可讀,也可寫。有點兒類似Stream,但是Stream是單向的。應用程序中不能直接對Channel進行讀寫操作,而必須通過Buffer來進行。比如,在讀一個Channel的時候,需要先將數據讀入到相對應的Buffer,然后在Buffer中進行讀取。
文件復制示例如下:
public static void nioCopyFile(String resource, String destination){ try { FileInputStream fis = new FileInputStream(resource); FileOutputStream fos = new FileOutputStream(destination); FileChannel readChannel = fis.getChannel(); FileChannel writeChannel = fos.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(1024); int len; while((len=readChannel.read(buffer))!=-1){ buffer.flip(); writeChannel.write(buffer); buffer.clear(); } readChannel.close(); writeChannel.close(); } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }
文件映射到內存
NIO提供了一種將文件映射到內存的方法進行I/O操作,它可以比常規的基於流的I/O快很多。這個操作主要由FileChannel.map()方法實現,比如:
MappredByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
以上代碼將文件的前1024個字節映射到內存中。map()方法返回一個MappredByteBuffer,它是ByteBuffer的子類。因此,可以像使用ByteBuffer那樣使用它。
public static void nioCopyFile(String resource, String destination){ try { RandomAccessFile fis = new RandomAccessFile(resource, "rw"); RandomAccessFile fos = new RandomAccessFile(destination, "rw"); FileChannel readChannel = fis.getChannel(); FileChannel writeChannel = fos.getChannel(); MappedByteBuffer mbb = readChannel.map(FileChannel.MapMode.READ_WRITE, 0, readChannel.size()); writeChannel.write(mbb); readChannel.close(); writeChannel.close(); } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }
或者如下寫法:
public static void nioCopyFile(String resource, String destination){ try { File res = new File(resource); File dest = new File(destination); if(!dest.exists()) dest.createNewFile(); FileInputStream fis = new FileInputStream(res); FileOutputStream fos = new FileOutputStream(dest); FileChannel readChannel = fis.getChannel(); FileChannel writeChannel = fos.getChannel(); MappedByteBuffer mbb = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, readChannel.size()); writeChannel.write(mbb); readChannel.close(); writeChannel.close(); } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }
或者如下寫法:
public static void nioCopyFile(String resource, String destination){ try { FileInputStream fis = new FileInputStream(resource); FileOutputStream fos = new FileOutputStream(destination); FileChannel readChannel = fis.getChannel(); FileChannel writeChannel = fos.getChannel(); MappedByteBuffer mbb = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, readChannel.size()); writeChannel.write(mbb); readChannel.close(); writeChannel.close(); } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }
處理結構化數據
NIO還提供了處理結構化數據的方法,稱之為散射(Scattering)和聚集(Gathering)。散射指將數據讀入一組Buffer中,而不僅僅是一個。聚集與之相反,指將數據寫入一組Buffer中。
假設有文本文件,格式為“書名作者”,現通過聚集寫操作創建該文件和散射讀文件:
public static void readAndWrite(){ try { //聚集寫操作 ByteBuffer bookBuf = ByteBuffer.wrap("java性能優化技巧".getBytes("utf-8")); ByteBuffer autBuf = ByteBuffer.wrap("葛一鳴".getBytes("utf-8")); int booklen = bookBuf.limit(); //記錄書名長度 int authlen = autBuf.limit(); //記錄作者長度 ByteBuffer[] bufs = new ByteBuffer[]{bookBuf, autBuf}; File file = new File("D:\\book.txt"); if(!file.exists()) file.createNewFile(); //文件不存在則創建文件 FileOutputStream fos = new FileOutputStream(file); FileChannel fc = fos.getChannel(); fc.write(bufs); fos.close(); fc.close(); //散射讀操作 ByteBuffer b1 = ByteBuffer.allocate(booklen); ByteBuffer b2 = ByteBuffer.allocate(authlen); ByteBuffer[] buffs = new ByteBuffer[]{b1,b2}; FileInputStream fin = new FileInputStream(file); FileChannel fic = fin.getChannel(); fic.read(buffs); fin.close(); fic.close(); String bookName = new String(b1.array(), "utf-8"); String authName = new String(b2.array(), "utf-8"); System.out.println(bookName+" "+authName);; } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }
直接內存訪問
NIO的Buffer還提供了一個可以直接訪問系統物理內存的類——DirectBuffer。DirectBuffer繼承自ByteBuffer,但和普通的ByteBuffer不同。普通的ByteBuffer仍然在JVM堆上分配空間,其最大內存,受到最大堆的限制。而DirectBuffer直接分配在物理內存中,並不占用堆空間。使用DirectBuffer是一個更接近系統底層的方法,所以,它的速度比普通的ByteBuffer更快。
申請DirectBuffer的方法如下:
ByteBuffer b = ByteBuffer.allocateDirect(500);
雖然訪問速度上有優勢,但是創建和銷毀DirectBuffer的花費卻遠比ByteBuffer高。因此在需要頻繁創建Buffer的場合,不宜使用DirectBuffer,但是如果能將DirectBuffer進行復用,那么,在讀寫頻繁的情況下,它完全可以大幅改善系統性能。
將DirectBuffer應用於真實系統中,不可避免地還需要對DirectBuffer進行監控。下面是一段可用於DirectBuffer監控的代碼,增強DirectBuffer的可用性:
//這段代碼用於監控DirectBuffer的使用情況 public void monDirectBuffer() throws ClassNotFoundException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException{ Class c = Class.forName("java.nio.Bits"); //通過反射取得私有數據 Field maxMemory = c.getDeclaredField("maxMemory"); maxMemory.setAccessible(true); Field reservedMemory = c.getDeclaredField("reservedMemory"); reservedMemory.setAccessible(true); synchronized(c){ Long maxMemoryValue = (Long) maxMemory.get(null); //總大小 Long reservedMemoryValue = (Long) reservedMemory.get(null); //剩余大小 System.out.println("maxMemoryValue:"+maxMemoryValue); System.out.println("reservedMemoryValue:"+reservedMemoryValue); } }