由于這些文件通道是異步工作的,因此與常規I / O相比它們的性能很有意思。 第二部分處理諸如內存和CPU消耗之類的問題,并說明如何在高性能方案中安全地使用新的NIO.2通道。 您還需要了解如何在不丟失數據的情況下關閉異步通道,這是第三部分。 最后,在第四部分中,我們將研究并發性。
注意:我不會解釋異步文件通道的完整API。 那里有足夠的帖子在這方面做得很好。 我的帖子更深入地介紹了實用性和使用異步文件通道時可能遇到的問題。
好吧,足夠模糊的談話,讓我們開始吧。 這是一個代碼片段,它打開一個異步通道(第7行),將字節序列寫入文件的開頭(第9行),并等待結果返回(第10行)。 最后,在第14行中關閉通道。
public class CallGraph_Default_AsynchronousFileChannel {private static AsynchronousFileChannel fileChannel;public static void main(String[] args) throws InterruptedException, IOException, ExecutionException {try {fileChannel = AsynchronousFileChannel.open(Paths.get("E:/temp/afile.out"), StandardOpenOption.READ,StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.DELETE_ON_CLOSE);Future<Integer> future = fileChannel.write(ByteBuffer.wrap("Hello".getBytes()), 0L);future.get();} catch (Exception e) {e.printStackTrace();} finally {fileChannel.close();}}
}
異步文件通道調用的重要參與者
在繼續研究代碼之前,讓我們快速介紹一下異步(文件)通道星系中涉及的概念。 圖1中的調用圖顯示了對AsynchronousFileChannel類的open()方法的調用中的序列圖。 FileSystemProvider封裝所有操作系統詳細信息。 為了逗大家,我在編寫本文時正在使用Windows XP客戶端。 因此,WindowsFileSystemProvider調用實際創建文件的WindowsChannelFactory并調用WindowsAsynchronousFileChannelImpl,后者返回其自身的實例。 最重要的概念是Iocp,即I / O完成端口。 它是用于執行多個同時異步輸入/輸出操作的API。 創建完成端口對象,并將其與許多文件句柄關聯。 當在對象上請求I / O服務時,將通過排隊到I / O完成端口的消息來指示完成。 不向其他請求I / O服務的進程通知I / O服務已完成,而是檢查I / O完成端口的消息隊列以確定其I / O請求的狀態。 I / O完成端口管理多個線程及其并發。 從圖中可以看出Iocp是AsynchronousChannelGroup的子類型。 因此,在JDK 7異步通道中,異步通道組被實現為I / O完成端口。 它擁有負責執行所請求的異步I / O操作的ThreadPool。 ThreadPool實際上封裝了ThreadPoolExecutor,它執行Java 1.5以來的所有多線程異步任務執行管理。 對異步文件通道的寫操作將導致對ThreadPoolExecutor.execute()方法的調用。
一些基準
查看性能總是很有趣。 異步非阻塞I / O必須快速,對嗎? 為了找到該問題的答案,我進行了基準分析。 同樣,我使用亨氏微小的基準框架來做到這一點。 我的機器是2.90 GHz的Intel Core i5-2310 CPU,具有四個內核(64位)。 在基準測試中,我需要一個基準。 我的基線是對普通文件的簡單常規同步寫入操作。 這是代碼段:
public class Performance_Benchmark_ConventionalFileAccessExample_1 implements Runnable {private static FileOutputStream outputfile;private static byte[] content = "Hello".getBytes();public static void main(String[] args) throws InterruptedException, IOException {try {System.out.println("Test: " + Performance_Benchmark_ConventionalFileAccessExample_1.class.getSimpleName());outputfile = new FileOutputStream(new File("E:/temp/afile.out"), true);Average average = new PerformanceHarness().calculatePerf(new PerformanceChecker(1000, new Performance_Benchmark_ConventionalFileAccessExample_1()), 5);System.out.println("Mean: " + DecimalFormat.getInstance().format(average.mean()));System.out.println("Std. Deviation: " + DecimalFormat.getInstance().format(average.stddev()));} catch (Exception e) {e.printStackTrace();} finally {new SystemInformation().printThreadInfo(true);outputfile.close();new File("E:/temp/afile.out").delete();}}@Overridepublic void run() {try {outputfile.write(content); // append content} catch (IOException e) {e.printStackTrace();}}
}
正如您在第25行中看到的那樣,基準測試將對普通文件執行一次寫入操作。 這些是結果:
Test: Performance_Benchmark_ConventionalFileAccessExample_1
Warming up ...
EPSILON:20:TESTTIME:1000:ACTTIME:1014:LOOPS:365947
EPSILON:20:TESTTIME:1000:ACTTIME:1014:LOOPS:372298
Starting test intervall ...
EPSILON:20:TESTTIME:1000:ACTTIME:1000:LOOPS:364706
EPSILON:20:TESTTIME:1000:ACTTIME:1014:LOOPS:368309
EPSILON:20:TESTTIME:1000:ACTTIME:1014:LOOPS:370288
EPSILON:20:TESTTIME:1000:ACTTIME:1001:LOOPS:364908
EPSILON:20:TESTTIME:1000:ACTTIME:1014:LOOPS:370820
Mean: 367.806,2
Std. Deviation: 2.588,665
Total started thread count: 12
Peak thread count: 6
Deamon thread count: 4
Thread count: 5
以下代碼段是另一個基準,該基準也向異步文件通道發出寫操作(第25行):
public class Performance_Benchmark_AsynchronousFileChannel_1 implements Runnable {private static AsynchronousFileChannel outputfile;private static int fileindex = 0;public static void main(String[] args) throws InterruptedException, IOException {try {System.out.println("Test: " + Performance_Benchmark_AsynchronousFileChannel_1.class.getSimpleName());outputfile = AsynchronousFileChannel.open(Paths.get("E:/temp/afile.out"), StandardOpenOption.WRITE,StandardOpenOption.CREATE, StandardOpenOption.DELETE_ON_CLOSE);Average average = new PerformanceHarness().calculatePerf(new PerformanceChecker(1000,new Performance_Benchmark_AsynchronousFileChannel_1()), 5);System.out.println("Mean: " + DecimalFormat.getInstance().format(average.mean()));System.out.println("Std. Deviation: " + DecimalFormat.getInstance().format(average.stddev()));} catch (Exception e) {e.printStackTrace();} finally {new SystemInformation().printThreadInfo(true);outputfile.close();}}@Overridepublic void run() {outputfile.write(ByteBuffer.wrap("Hello".getBytes()), fileindex++ * 5);}
}
這是我的機器上上述基準測試的結果:
Test: Performance_Benchmark_AsynchronousFileChannel_1
Warming up ...
EPSILON:20:TESTTIME:1000:ACTTIME:1015:LOOPS:42667
EPSILON:20:TESTTIME:1000:ACTTIME:1015:LOOPS:193351
Starting test intervall ...
EPSILON:20:TESTTIME:1000:ACTTIME:1015:LOOPS:191268
EPSILON:20:TESTTIME:1000:ACTTIME:1015:LOOPS:186916
EPSILON:20:TESTTIME:1000:ACTTIME:1014:LOOPS:189842
EPSILON:20:TESTTIME:1000:ACTTIME:1014:LOOPS:191103
EPSILON:20:TESTTIME:1000:ACTTIME:1015:LOOPS:192005
Mean: 190.226,8
Std. Deviation: 1.795,733
Total started thread count: 17
Peak thread count: 11
Deamon thread count: 9
Thread count: 10
由于上面的代碼片段執行相同的操作,因此可以肯定地說異步文件通道不一定比常規I / O更快。 我認為這是一個有趣的結果。 在單線程基準測試中很難將常規I / O和NIO.2相互比較。 引入NIO.2是為了在高度并發的場景中提供I / O技術。 因此,詢問更快的速度(NIO或常規I / O)并不是一個正確的問題。 合適的問題是:什么是“更多并發”? 但是,就目前而言,以上結果表明:
當只有一個線程發出I / O操作時,請考慮使用常規I / O。
現在就足夠了。 我已經解釋了基本概念,還指出了常規I / O仍然存在。 在第二篇文章中,我將介紹使用默認異步文件通道時可能遇到的一些問題。 我還將展示如何通過應用一些更可行的設置來避免這些問題。
應用自定義線程池
異步文件處理并不是高性能的綠卡。 在上一篇文章中,我證明了常規I / O可以比異步通道更快。 應用NIO.2文件通道時,還需要了解一些其他重要事實。 默認情況下,在NIO.2文件通道中執行所有異步I / O任務的Iocp類由所謂的“緩存”線程池支持。 這是一個線程池,可以根據需要創建新線程,但是會在可用時重用以前構造的線程。 查看Iocp持有的ThreadPool類的代碼。
public class ThreadPool {
...private static final ThreadFactory defaultThreadFactory = new ThreadFactory() {@Overridepublic Thread newThread(Runnable r) {Thread t = new Thread(r);t.setDaemon(true);return t;}};
...static ThreadPool createDefault() {...ExecutorService executor =new ThreadPoolExecutor(0, Integer.MAX_VALUE,Long.MAX_VALUE, TimeUnit.MILLISECONDS,new SynchronousQueue<Runnable>(),threadFactory);return new ThreadPool(executor, false, initialSize);}
...
}
默認通道組中的線程池被構造為ThreadPoolExecutor,最大線程數為Integer.MAX_VALUE,保持時間為Long.MAX_VALUE。 線程由線程工廠創建為守護程序線程。 如果所有線程都忙,則使用同步移交隊列來觸發線程創建。 此配置存在多個問題:
- 如果您在異步通道上突發執行寫入操作,則將創建數千個工作線程,這可能會導致OutOfMemoryError:無法創建新的本機線程。
- 當JVM退出時,所有守護進程線程都將被放棄-最終不執行塊,也不會取消堆棧。
在我的其他博客中,我解釋了為什么無限制線程池會引起麻煩。 因此,如果您使用異步文件通道,則可以選擇使用自定義線程池而不是默認線程池。 以下代碼段顯示了示例自定義設置。
ThreadPoolExecutor pool = new
ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(2500));
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
AsynchronousFileChannel outputfile = AsynchronousFileChannel.open(Paths.get(FILE_NAME), new HashSet<Standardopenoption>
(Arrays.asList(StandardOpenOption.WRITE, StandardOpenOption.CREATE)), pool);
AsynchronousFileChannel的Javadoc指出,自定義執行程序應“至少[...]支持無限制的工作隊列,并且不應在execute方法的調用者線程上運行任務”。 這是一個冒險的說法,只有在資源不成問題的情況下才是合理的,這種情況很少發生。 對于異步文件通道,請使用有限線程池。 您不會遇到線程太多的問題,也無法用工作隊列任務來充斥您的堆。 在上面的示例中,您有五個線程執行異步I / O任務,并且工作隊列可容納2500個任務。 如果超過了容量限制,則拒絕執行處理程序將實現CallerRunsPolicy,在該處客戶端必須同步執行寫任務。 因為工作負載被“推回”到客戶端并同步執行,所以這可能(極大地)降低系統性能。 但是,它也可以使您免受結果無法預測的更嚴重的問題的困擾。 最佳做法是使用有界線程池并保持線程池大小可配置,以便您可以在運行時進行調整。 同樣,要了解有關可靠的線程池設置的更多信息,請參閱我的其他博客條目。
具有同步移交隊列和未限制最大線程池大小的線程池可能會激進地創建新線程,因此,通過消耗(PC寄存器和Java堆棧)JVM的運行時內存,可能會嚴重損害系統穩定性。 異步任務的“時間越長”(經過的時間),您越有可能遇到此問題。
具有無限制工作隊列和固定線程池大小的線程池可以激進地創建新的任務和對象,從而通過過多的垃圾回收活動消耗堆內存和CPU,從而嚴重損害系統穩定性。 異步任務越大(大小)越長(經過時間),您越有可能遇到此問題。
這就是將自定義線程池應用于異步文件通道的全部內容。 我在本系列的下一篇博客中將介紹如何安全地關閉異步通道而不丟失數據。
參考:測試平臺上的Java 7#7:NIO.2文件通道–第1部分–簡介,測試平臺上的Java 7#8:NIO.2文件通道–第2部分–應用來自我們JCG合作伙伴 Niklas的自定義線程池。
翻譯自: https://www.javacodegeeks.com/2012/04/java-7-8-nio2-file-channels-on-test.html