NIO.2另一個新特性是異步能力,這種能力對套接字和文件I/O都適用。異步I/O其實只是一種在讀寫操作結束前允許進行其他操作的I/O處理。實際上,就是可以充分利用最新的硬件和軟件特性,比如多核CPU及操作系統對套接字和文件處理的支持。對於任何想在服務器端和系統級編程領域佔有一席之地的編程語言來說,異步I/O都是必不可少的特性。我們相信,Java在服務器端編程語言中所取得的重要地位會因為該特性得以延續。
舉個簡單的例子,想像一下你要把100GB的數據寫入文件系統或網絡套接字中。如果你用的是老版本的Java,在同時把數據寫入文件或套接字的多個區域時,必須親自動手用java.util.concurrent
寫多線程代碼。當然,同時進行多路讀取也不容易。除非你寫的代碼十分巧妙,否則在使用I/O時也會阻塞主線程,這意味著在你完成漫長的I/O操作之前,除了等待,還是等待。
提示 如果你還沒接觸過NIO通道,也許可以趁此機會充實下你的知識結構。不過這個領域的新內容很少,但在繼續本節內容之前,我們建議你去看看Ron Hitchens寫的Java NIO(O\'Reilly,2002)一書,你會從中獲益匪淺。
Java 7中有三個新的異步通道:
AsynchronousFileChannel
——用於文件I/O;AsynchronousSocketChannel
——用於套接字I/O,支持超時;AsynchronousServerSocketChannel
——用於套接字接受異步連接。
使用新的異步I/O API時,主要有兩種形式,將來式和回調式。有趣的是,這些異步API用到了第4章討論的一些現代並發技術,所以這真是讓你先睹為快了。
我們會從異步文件訪問的將來式開始。希望你已經用過這種並發技術,但如果沒有用過,也不用擔心,本節將會講解得非常詳細,即便是剛接觸這個話題的新手也能看明白。
2.5.1 將來式
NIO.2 API的設計人員用將來(future)式這個術語來表明使用java.util.concurrent.Future
接口。當你希望由主控線程發起I/O操作並輪詢等待結果時,一般都會用將來式異步處理。
將來式用現有的java.util.concurrent
技術聲明一個Future
,用來保存異步操作的處理結果。這很關鍵,因為這意味著當前線程不會因為比較慢的I/O操作而停滯。相反,有一個單獨的線程發起I/O操作,並在操作完成時返回結果。與此同時,主線程可以繼續執行其他需要完成的任務。在其他任務結束後,如果I/O操作還沒有完成,主線程會一直等待。圖2-3演示了一個用將來式讀取大型文件的過程。(代碼清單2-8是相應的實現代碼。)
圖2-3 將來式異步讀取
通常會用Future get
方法(帶或不帶超時參數)在異步I/O操作完成時獲取其結果。假設你要從硬盤上的文件裡讀取100 000個字節,在舊版的Java中,你需要等待數據讀取完成(除非你實現了一個線程池,而且工作線程使用java.util.concurrent
技術,這可不是件輕鬆的事兒)。而在Java 7中,主線程可以在讀取數據的同時繼續完成其他工作,如下面的代碼所示。
代碼清單2-8 異步I/O——將來式
try
{
Path file = Paths.get(\"/usr/karianna/foobar.txt\");
AsynchronousFileChannel channel =
AsynchronousFileChannel.open(file); //1 異步打開文件
/**2讀取100 000字節*/
ByteBuffer buffer = ByteBuffer.allocate(100_000);
Future<Integer> result = channel.read(buffer,0);
while(!result.isDone)
{
ProfitCalculator.calculateTax;//3幹點兒別的事情
}
Integer bytesRead = result.get; //4獲取結果
System.out.println(\"Bytes read [\" + bytesRead + \"]\");
}
catch (IOException | ExecutionException | InterruptedException e)
{
System.out.println(e.getMessage);
}
上面的代碼一開始先用後台進程中打開一個AsynchronousFileChannel
讀/寫foobar.txt1。接下來的這一步是為了讓I/O處理能跟發起它的線程同步進行。因為採用AsynchronousFileChannel
,並用Future
保存讀取結果,所以會自動採用並發的I/O處理2。在讀取數據時,主線程可以繼續執行任務(比如算一下要交多少稅)3。最後,當任務完成時,你可以檢查數據讀取結果4。
一定要注意,我們在這裡用isDone
手工判定result是否結束。通常情況下,result或結束(主線程會繼續執行),或等待後台I/O完成。
你可能會好奇這究竟是怎麼實現的。長話短說,API/JVM為執行這個任務創建了線程池和通道組。另外,你也可以自己提供和配置一個。解釋其中的細節頗費口舌,並且官方文檔都解釋過了,所以我們只是直接引用了AsynchronousFileChannel
的Javadoc:
AsynchronousFileChannel
會關聯線程池,它的任務是接收I/O處理事件,並分發給負責處理通道中I/O操作結果的結果處理器。跟通道中發起的I/O操作關聯的結果處理器確保是由線程池中的某個線程產生的。
如果在創建AsynchronousFileChannel
時沒有為其指明線程池,那就會為其分配一個系統默認的線程池(可能會和其他通道共享)。默認線程池是由AsynchronousChannelGroup
類定義的系統屬性進行配置的。
此外還有一種被稱為回調的技術。有些開發人員可能會發現回調式用起來更方便,因為它很像Swing、消息和其他Java API中出現過的事件處理技術。
2.5.2 回調式
與將來式相反,回調(callback)式所採用的事件處理技術類似於在Swing UI編程時採用的機制。其基本思想是主線程會派一個偵查員CompletionHandler
到獨立的線程中執行I/O操作。這個偵查員將帶著I/O操作的結果返回到主線程中,這個結果會觸發它自己的completed
或failed
方法(你會重寫這兩個方法)。
在異步事件剛一成功或失敗並需要馬上採取行動時,一般會用回調式。比如在讀取對盈利計算業務處理至關重要的金融數據時,如果讀取失敗了,你最好馬上就執行回滾操作,或進行異常處理。
在異步I/O活動結束後,接口java.nio.channels.CompletionHandler<V,A>
會被調用,其中V是結果類型,A是提供結果的附著對象。此時必須已經有了該接口的completed(V,A)
和failed(V,A)
方法的實現,你的程序才能知道在異步I/O操作成功完成或因某些原因失敗時該如何處理。圖2-4展示了這一過程(代碼清單2-9是該過程的實現代碼)。
圖2-4 回調式異步讀取
在下例中,你又一次從foobar.txt文件中讀取了100 000字節的數據,用CompletionHandler<Integer,ByteBuffer>
聲明是成功或是失敗。
代碼清單2-9 異步 I/O——回調式
try
{
Path file = Paths.get(\"/usr/karianna/foobar.txt\")
AsynchronousFileChannel channel =
AsynchronousFileChannel.open(file); //以異步方式打開文件
ByteBuffer buffer = ByteBuffer.allocate(100_000);
/**從通道中讀取數據*/
channel.read(buffer, 0, buffer,
new CompletionHandler<Integer, ByteBuffer>
{
/**讀取完成時的回調方法*/
public void completed(Integer result,
ByteBuffer attachment)
{
System.out.println(\"Bytes read [\" + result + \"]\");
}
public void failed(Throwable exception, ByteBuffer attachment)
{
System.out.println(exception.getMessage);
}
});
}
catch (IOException e)
{
System.out.println(e.getMessage);
}
本節中的兩個例子都是基於文件的,但將來式和回調式異步訪問也適用於 AsynchronousServerSocketChannel
和AsynchronousSocketChannel
。開發人員可以用它們編寫程序來處理網絡套接字,比如語音IP或寫出性能更優異的客戶端和服務器端軟件。
接下來的一系列變化統一了套接字和通道,讓你可以將套接字和通道交互的管理歸結到API中。