Java NIO(New Input/Output)――新的輸入/輸出API套件――是2002年引進到J2SE 1.4裡的。 Java NIO的目標是提高Java平台上的I/O密集型任務的效能。過了十年,很多Java開發者還是不知道怎麼充分利用NIO,更少的人知道在Java SE 7裡引進了更新的輸入/輸出API(NIO.2)。 NIO和NIO.2對於Java平台最大的貢獻是提高了Java應用開發中的一個核心元件的效能:輸入/輸出處理。不過這兩個包都不是很好用,而且它們也不是適用於所有的場景。如果能夠正確地使用的話,Java NIO和NIO.2可以大幅減少一些常用I/O操作所花費的時間。這就是NIO和NIO.2所擁有的超能力,我會在這篇文章裡向你展示5種使用它們的簡單方法。
變更通知(因為每個事件都需要一個監聽者)
選擇器和非同步IO:透過選擇器來提高多路復用
通道――承諾與現實
記憶體映射――好鋼用在刀刃上
字符編碼和搜尋
NIO的背景
為什麼一個已經存在10年的增強包還是Java的新I/O包呢?原因是對大多數的Java程式設計師而言,基本的I/O操作都能夠勝任。在日常工作中,大部分的Java開發者沒有必要去學習NIO。更進一步,NIO不只是一個效能提升包。相反,它是一個和Java I/O相關的不同函數的集合。 NIO透過使得Java應用的效能「更接近實質」來達到效能提升的效果,也就是意味著NIO和NIO.2的API暴露了低層次的系統運作的入口。 NIO的代價就是它在提供更強大的I/O控制能力的同時,也要求我們比使用基本的I/O程式更細心地使用和練習。 NIO的另一個特點是它對於應用程式的表現力的關注,這個我們將在下面的練習中看到。
開始學習NIO和NIO.2
NIO的參考資料非常多――參考資料中選取的一些連結。要學習NIO和NIO.2的話,Java 2 SDK Standard Edition(SE) documentation 和Java SE 7 documentation 都是不可或缺的。要使用這篇文章裡的程式碼,你需要使用JDK 7或更高的版本。
對於許多開發者而言,它們第一次遇到NIO都可能是在維護應用程式的時候:一個功能正常的應用程式回應越來越慢,因此有人建議使用NIO來提高回應速度。 NIO在提升應用效能的時候顯得比較出眾,不過具體的結果取決於底層系統.(注意NIO是平台相關的)。如果你是第一次使用NIO的話,你需要仔細衡量。你會發現NIO提升效能的能力不只取決於OS,同時也取決於你所使用的JVM,主機的虛擬上下文,大容量儲存的特性甚至和資料也是相關的。因此,性能衡量的工作是比較難做的。尤其是當你的系統存在一個可移動的部署環境的時候,你需要特別注意。
了解了上面的內容後,我們沒有後顧之憂了,現在就來體驗NIO和NIO.2的5個重要的功能。
1. 變更通知(因為每個事件都需要一個監聽者)
對NIO和NIO.2有興趣的開發者的共同關注點在於Java應用的效能。根據我的經驗,NIO.2裡的檔案變更通知者(file change notifier)是新輸入/輸出API裡最讓人感興趣(被低估了)的特性。
很多企業級應用程式需要在下面的情況時做一些特殊的處理:
當一個檔案上傳到一個FTP資料夾裡時
當一個配置裡的定義被修改時
當一個草稿文檔被上傳時
其他的檔案系統事件出現時
這些都是變更通知或變更回應的例子。在Java(以及其他語言)的早期版本裡,輪詢(polling)是偵測這些變更事件的最佳方式。輪詢是一種特殊的無限循環:檢查檔案系統或其他對象,並且和之前的狀態對比,如果沒有變化,在大概幾百個毫秒或10秒的間隔後,繼續檢查。就這一直無限循環下去。
NIO.2提供了一個更好地方式來進行變更檢測。列表1是一個簡單的範例。
列表1. NIO.2裡的變更通知機制
複製代碼代碼如下:
import java.nio.file.attribute.*;
importjava.io.*;
importjava.util.*;
importjava.nio.file.Path;
importjava.nio.file.Paths;
importjava.nio.file.StandardWatchEventKinds;
importjava.nio.file.WatchEvent;
importjava.nio.file.WatchKey;
importjava.nio.file.WatchService;
importjava.util.List;
publicclassWatcher{
publicstaticvoidmain(String[]args){
Paththis_dir=Paths.get(".");
System.out.println("Nowwatchingthecurrentdirectory...");
try{
WatchServicewatcher=this_dir.getFileSystem().newWatchService();
this_dir.register(watcher,StandardWatchEventKinds.ENTRY_CREATE);
WatchKeywatckKey=watcher.take();
List<WatchEvent<<64;>>events=watckKey.pollEvents();
for(WatchEventevent:events){
System.out.println("Someonejustcreatedthefile'"+event.context().toString()+"'.");
}
}catch(Exceptione){
System.out.println("Error:"+e.toString());
}
}
}
編譯這段程式碼,然後在命令列執行。在相同的目錄下,建立一個新的文件,例如執行touchexample或copyWatcher.classexample指令。你會看到下面的變更通知訊息:
Someonejustcreatethefiel'example1′.
這個簡單的範例展示了怎麼開始使用JavaNIO的功能。同時,它也介紹了NIO.2的Watcher類,它相比較原始的I/O中的輪詢方案而言,顯得更加直接和易用。
注意拼字錯誤
當你從這篇文章裡拷貝程式碼時,注意拼字錯誤。例如,列表1種的StandardWatchEventKinds物件是複數的形式。即使在Java.net的文檔裡都把它給拼字錯了。
小技巧
NIO裡的通知機制比舊的輪詢方式使用起來更簡單,這會誘導你忽略對特定需求的詳細分析。當你在你第一次使用監聽器的時候,你需要仔細考慮你所使用的這些概念的語意。例如,知道一個變更什麼時候會結束比知道它什麼時候開始更重要。這種分析需要非常仔細,尤其是像移動FTP資料夾這種常見的場景。 NIO是一個功能非常強大的包,但同時它還會有一些微妙的“陷阱”,這會給那些不熟悉它的人帶來困擾。
2.選擇器和非同步IO:透過選擇器來提高多路復用
NIO新手一般都把它和「非阻塞輸入/輸出」連結在一起。 NIO不僅僅只是非阻塞I/O,不過這種認知也不完全是錯的:Java的基本I/O是阻塞式I/O――意味著它會一直等待到操作完成――然而,非阻塞或異步I/O是NIO裡最常使用的一個特點,而非NIO的全部。
NIO的非阻塞I/O是事件驅動的,並且在列表1里文件系統監聽範例裡進行了展示。這意味著給一個I/O通道定義一個選擇器(回呼或監聽器),然後程式可以繼續運作。當一個事件發生在這個選擇器上時――例如接收到一行輸入――選擇器會「醒來」並且執行。所有的這些都是透過一個單線程來實現的,這和Java的標準I/O有著顯著的差異的。
列表2裡面展示了使用NIO的選擇器實現的一個多埠的網路程式echo-er,這裡是修改了GregTravis在2003年創建的一個小程式(參考資源清單)。 Unix和類別Unix系統很早就實現高效的選擇器,它是Java網路高效能程式設計模型的一個很好的參考模型。
列表2.NIO選擇器
複製代碼代碼如下:
importjava.io.*;
importjava.net.*;
importjava.nio.*;
importjava.nio.channels.*;
importjava.util.*;
publicclassMultiPortEcho
{
privateintports[];
privateByteBufferechoBuffer=ByteBuffer.allocate(1024);
publicMultiPortEcho(intports[])throwsIOException{
this.ports=ports;
configure_selector();
}
privatevoidconfigure_selector()throwsIOException{
//Createanewselector
Selectorselector=Selector.open();
//Openalisteneroneachport,andregistereachone
//withtheselector
for(inti=0;i<ports.length;++i){
ServerSocketChannelssc=ServerSocketChannel.open();
ssc.configureBlocking(false);
ServerSocketss=ssc.socket();
InetSocketAddressaddress=newInetSocketAddress(ports[i]);
ss.bind(address);
SelectionKeykey=ssc.register(selector,SelectionKey.OP_ACCEPT);
System.out.println("Goingtolistenon"+ports[i]);
}
while(true){
intnum=selector.select();
SetselectedKeys=selector.selectedKeys();
Iteratorit=selectedKeys.iterator();
while(it.hasNext()){
SelectionKeykey=(SelectionKey)it.next();
if((key.readyOps()&SelectionKey.OP_ACCEPT)
==SelectionKey.OP_ACCEPT){
//Acceptthenewconnection
ServerSocketChannelssc=(ServerSocketChannel)key.channel();
SocketChannelsc=ssc.accept();
sc.configureBlocking(false);
//Addthenewconnectiontotheselector
SelectionKeynewKey=sc.register(selector,SelectionKey.OP_READ);
it.remove();
System.out.println("Gotconnectionfrom"+sc);
}elseif((key.readyOps()&SelectionKey.OP_READ)
==SelectionKey.OP_READ){
//Readthedata
SocketChannelsc=(SocketChannel)key.channel();
//Echodata
intbytesEchoed=0;
while(true){
echoBuffer.clear();
intnumber_of_bytes=sc.read(echoBuffer);
if(number_of_bytes<=0){
break;
}
echoBuffer.flip();
sc.write(echoBuffer);
bytesEchoed+=number_of_bytes;
}
System.out.println("Echoed"+bytesEchoed+"from"+sc);
it.remove();
}
}
}
}
staticpublicvoidmain(Stringargs[])throwsException{
if(args.length<=0){
System.err.println("Usage:javaMultiPortEchoport[portport...]");
System.exit(1);
}
intports[]=newint[args.length];
for(inti=0;i<args.length;++i){
ports[i]=Integer.parseInt(args[i]);
}
newMultiPortEcho(ports);
}
}
編譯這段程式碼,然後透過類似javaMultiPortEcho80058006這樣的指令來啟動它。一旦這個程式運作成功,啟動一個簡單的telnet或其他的終端模擬器來連接8005和8006介面。你會看到這個程式會回顯它接收到的所有字元――而且它是透過一個Java執行緒來實現的。
3.通道:承諾與現實
在NIO裡,一個通道(channel)可以表示任何可以讀寫的物件。它的作用是為檔案和套介面提供抽象。 NIO通道支援一系列一致的方法,這樣就使得編碼的時候不需要去特別關心不同的對象,無論它是標準輸出,網路連接還是正在使用的通道。通道的這個特性是繼承自Java基本I/O中的流(stream)。流(stream)提供了阻塞式的IO;通道支援異步I/O。
NIO常常因為它的效能高而被推薦,不過更精確地是因為它的反應快速。在有些場景下NIO會比基本的JavaI/O的效能差。例如,對於一個小檔案的簡單的順序讀寫,簡單透過串流來實現的效能可能比對應的面向事件的基於通道的編碼實現的快兩到三倍。同時,非多路復用(non-multiplex)的通道――也就是每個執行緒一個單獨的通道――要比多個通道把各自的選擇器註冊在同一個執行緒裡要慢多了。
下面你在考慮要使用串流還是通道的時候,試著問自己下面幾個問題:
你需要讀寫多少個I/O物件?
不同的I/O物件直接是否有順序,還是他們都需要同時發生的?
你的I/O物件是需要持續一小段時間還是在你的進程的整個宣告週期都存在?
你的I/O是適合在單一線程裡處理還是在幾個不同的線程裡?
網路通訊和本地I/O是看起來一樣,還是各自有著不同的模式?
這樣的分析是決定使用流還是通道的一個最佳實踐。記住:NIO和NIO.2不是基本I/O的替代,而它的一個補充。
4.記憶體映射――好鋼用在刀刃上
NIO裡對效能提升最顯著的是記憶體映射(memorymapping)。記憶體映射是一個系統層面的服務,它把程式裡用到的檔案的一段當作記憶體來處理。
記憶體映射存在著許多潛在的影響,比我這裡提供的還要多。在一個更高的層次上,它能夠使得檔案存取的I/O的效能達到記憶體存取的速度。記憶體存取的速度往往比檔案存取的速度快幾個數量級。列表3是一個NIO記憶體映射的簡單範例。
列表3.NIO裡的記憶體映射
複製代碼代碼如下:
importjava.io.RandomAccessFile;
importjava.nio.MappedByteBuffer;
importjava.nio.channels.FileChannel;
publicclassmem_map_example{
privatestaticintmem_map_size=20*1024*1024;
privatestaticStringfn="example_memory_mapped_file.txt";
publicstaticvoidmain(String[]args)throwsException{
RandomAccessFilememoryMappedFile=newRandomAccessFile(fn,"rw");
//Mappingafileintomemory
MappedByteBufferout=memoryMappedFile.getChannel().map(FileChannel.MapMode.READ_WRITE,0,mem_map_size);
//WritingintoMemoryMappedFile
for(inti=0;i<mem_map_size;i++){
out.put((byte)'A');
}
System.out.println("File'"+fn+"'isnow"+Integer.toString(mem_map_size)+"bytesfull.");
//Readfrommemory-mappedfile.
for(inti=0;i<30;i++){
System.out.print((char)out.get(i));
}
System.out.println("/nReadingfrommemory-mappedfile'"+fn+"'iscomplete.");
}
}
在清單3中,這個簡單的範例建立了一個20M的檔案example_memory_mapped_file.txt,並且用字元A對它進行填充,然後讀取前30個位元組。在實際的應用中,記憶體映射不僅僅擅長提高I/O的原始速度,同時它也允許多個不同的reader和writer同時處理同一個檔案鏡像。這個技術功能強大但是也很危險,不過如果正確使用的話,它會讓你的IO速度提高數倍。眾所周知,華爾街的交易操作為了能夠贏得秒級甚至是毫秒的優勢,都使用了記憶體映射技術。
5.字符編碼和搜尋
我在這篇文章裡要講解的NIO的最後一個特性是charset,一個用來轉換不同字元編碼的套件。在NIO之前,Java透過getByte方法內建實作了大部分相同的功能。 charset很受歡迎,因為它比getBytes更靈活,並且能夠在更底層去實現,這樣就能夠獲得更好的性能。這個對於搜尋那些對於編碼、順序以及其他語言特徵比較敏感的非英語語言而言更加有價值。
列表4展示了一個把Java裡的Unicode字元轉換成Latin-1的範例
列表4.NIO裡的字符
複製代碼代碼如下:
Stringsome_string="ThisisastringthatJavanativelystoresasUnicode.";
Charsetlatin1_charset=Charset.forName("ISO-8859-1");
CharsetEncodelatin1_encoder=charset.newEncoder();
ByteBufferlatin1_bbuf=latin1_encoder.encode(CharBuffer.wrap(some_string));
注意Charset和通道被設計成能夠放在一起進行使用,這樣就能夠使得程式在記憶體映射、非同步I/O以及編碼轉換進行協作的時候,能夠正常運作。
總結:當然還有更多需要去了解
這篇文章的目的是為了讓Java開發者熟悉NIO和NIO.2裡的一些最主要(也是最有用)的功能。你可以透過這些範例建立起來的一些基礎來理解NIO的一些其他方法;例如,你所學習的關於通道的知識能夠幫助你去理解NIO的Path裡對於檔案系統裡的符號連結的處理。你也可以參考一下我後面給的資源列表,裡面給了一些深入學習Java新I/OAPI的文件。