IO操作幾乎對于所有的應用都是非常重要的,因為IO操作非常容易導致性能瓶頸。在Java的世界里存在兩大類(lèi)IO,傳統IO(TIO)和新IO(NIO)。外加一個(gè)即將到來(lái)的增強版的NIO——NIO2(JDK7)。 NIO(以及NIO2)主要用于在一些特定情況下增強性能、提供更好的操作系統層次IO功
IO操作幾乎對于所有的應用都是非常重要的,因為IO操作非常容易導致性能瓶頸。
在Java的世界里存在兩大類(lèi)IO,傳統IO(TIO)和新IO(NIO)。外加一個(gè)即將到來(lái)的增強版的NIO——NIO2(JDK7)。
NIO(以及NIO2)主要用于在一些特定情況下增強性能、提供更好的操作系統層次IO功能集成,但它們無(wú)法完全替代TIO!在許多情況下TIO仍然是你唯一的選擇。
今天我們就來(lái)討論一下TIO的性能問(wèn)題。
IO的性能瓶頸主要分為兩類(lèi):
錯誤的使用緩沖(buffer)
過(guò)度的同步保護
我們都知道buffer能夠增加IO的性能,但不是每個(gè)人都知道如何正確的使用buffer,在本文結束時(shí)我會(huì )給出一些最佳實(shí)踐建議。
第一部分:
對于1,錯誤的使用緩沖(buffer),存在兩點(diǎn)非常流行的錯誤用法和一個(gè)感念上的誤解
a)為內存IO類(lèi)(In-memory IO class)添加緩沖(錯誤用法)
b)為已添加buffer的IO類(lèi)再次添加buffer(錯誤用法)
c)Buffer版的IO類(lèi)和顯式使用buffer間的關(guān)系(概念上的誤解)
對于a),這是非?;闹嚨?!添加buffer的目的是為了將對IO設備的多次訪(fǎng)問(wèn)合并為一次訪(fǎng)問(wèn)從而提高性能,In-memory類(lèi)(如ByteArrayInput/OutputStream)根本就不會(huì )訪(fǎng)問(wèn)任何IO設備,所以對它們添加buffer是完全沒(méi)有必要的。
對于b),這是完全多余的!你只需要buffer一次就可以了,多于一次的buffer只會(huì )引入更多的棧調用和垃圾創(chuàng )建。
對于c),這需要多一點(diǎn)解釋?zhuān)?div style="height:15px;">
從本質(zhì)上講,這兩種做法是要達到相同的目的,但方法不同,這也導致了它們之間具有巨大的性能差異!
針對這一問(wèn)題我做了一個(gè)測試,比較使用Buffer版的IO類(lèi)和顯式使用buffer在讀/寫(xiě)文件時(shí)的性能差異。
下面的測試結果顯式了兩者的性能差異:
讀測試結果:(所有數據都是在JVM預熱后取得的,每個(gè)采樣點(diǎn)的時(shí)間是10次讀取操作的總時(shí)間,單位為毫秒)
文件大小 1K10K
100K
1M
10M
100M
1G
BufferedInputStream 0 1
5 53 549 5492 56002
顯式使用byte[]在FileInputStream上讀取 0 0 1
10
113
1126
11448
寫(xiě)測試結果:(所有數據都是在JVM預熱后取得的,每個(gè)采樣點(diǎn)的時(shí)間是10次寫(xiě)入操作的總時(shí)間,單位為毫秒)
文件大小 1K10K
100K
1M
10M
100M
1G
BufferedOutputStream 0 1
5 45 472 4793 48794
顯式使用byte[]在FileOuputStream上寫(xiě)入 0 1 1
10
124
1300
13138
為什么會(huì )有如此大的性能差異呢?有兩點(diǎn)原因:
Buffer版的IO類(lèi)導致很多無(wú)謂的棧調用(都是裝飾者模式惹得禍decorator pattern)
JDK中所有的Buffer版IO類(lèi)都是線(xiàn)程安全的,這就意味著(zhù)它們添加了大量的同步保護(將在第二部分中詳細解釋?zhuān)?div style="height:15px;">
現在你知道了顯式使用buffer要比使用Buffer版的IO類(lèi)具有更好的性能,所以請盡量多顯式使用buffer,但在兩種特殊情況下你仍然需要Buffer版的IO類(lèi):
當你在使用第三方庫時(shí),庫的api需要IO類(lèi)作為參數,并且你確定他們內部的代碼采用流式方式編碼(非塊式操作),也就是沒(méi)有顯式使用buffer。在不修改他們代碼的前提下,你只能通過(guò)傳入Buffer版的IO類(lèi)對象來(lái)提升性能。
我一點(diǎn)也不喜歡這個(gè)包里面的代碼,因為他們都是線(xiàn)程安全的,也就意味著(zhù)許多同步保護。如果我需要同步保護,我會(huì )自己去做,而且我絕不會(huì )去添加任何多余的保護。但在這一點(diǎn)上JDK的IO包把我逼得無(wú)路可走
只要你使用JDK的IO包,你就被迫的添加了許多同步保護,即使你完全確定你的代碼運行在單一線(xiàn)程的環(huán)境下,你也不能回避這些不必要的保護。你也許會(huì )好奇的問(wèn),這真的是一個(gè)嚴重的問(wèn)題嗎?JVM在運行時(shí)會(huì )對弱競爭的鎖進(jìn)行優(yōu)化,不是嗎?顯然,它做的優(yōu)化還不到家,讓我們來(lái)看一下性能測試結果。
我翻版了一批JDK IO包中常用的類(lèi),這個(gè)翻版完全是API層次的翻版,也就是說(shuō)全部代碼是參照JDK IO包的Javadoc寫(xiě)成, 沒(méi)有直接借用JDK的源碼。因為JDK源碼大部分使用GPL協(xié)議發(fā)布,而Liferay的代碼采用MIT協(xié)議發(fā)布,為了不引起IP糾紛只好照葫蘆畫(huà)瓢。而實(shí)際上從0開(kāi)始創(chuàng )建這些類(lèi)一點(diǎn)也不難(僅僅是裝飾者模式而已),只是非常的繁瑣。在我的翻版類(lèi)中,我移除了全部的同步保護。而我的測試也進(jìn)行在單一線(xiàn)程環(huán)境下,所以不用擔心線(xiàn)程安全的問(wèn)題。
測試包含兩部分,第一部分比較原始JDK IO類(lèi)和我的unsyc版的IO類(lèi)在讀取內存數據(In-memory data)時(shí)的性能差異,第二部分比較原始JDK IO類(lèi)和我的unsyc版的IO類(lèi)在寫(xiě)入內存數據(In-memory data)時(shí)的性能差異。之所以采用內存數據而不是磁盤(pán)數據是為了放大同步操作對整體性能的影響,以便于分析。
寫(xiě)數據的測試曲線(xiàn)不像讀的那樣平滑,原因在于它內部使用了一個(gè)動(dòng)態(tài)增長(cháng)的byte[],這導致大量GC活動(dòng)(與上一期Blog中我們討論SB時(shí)看到的問(wèn)題相似)。
好了,現在你應該看到了同步保護是一項多么沉重的操作。我們日常開(kāi)發(fā)中存在大量局限在方法調用棧內的IO類(lèi)使用,這些情況都是絕對發(fā)生在單一線(xiàn)程環(huán)境下的。另外一些時(shí)候,即使IO對象的引用超出了方法調用棧的作用域,但我們可以通過(guò)分析得知它仍然只會(huì )被單一線(xiàn)程所訪(fǎng)問(wèn),比如web開(kāi)發(fā)中,針對一個(gè)request的全部處理一般都是由一個(gè)worker thread來(lái)完成的(除非你的后臺還有其他的異步服務(wù)線(xiàn)程與worker間交換數據,但這很少見(jiàn))。對于這樣的情況,你大可以放心的使用這些unsyc的IO類(lèi)(com.liferay.portal.kernel.io.unsync)。
當你確定你的代碼運行在單一線(xiàn)程環(huán)境下,或者你自己添加了同步保護時(shí),請使用com.liferay.portal.kernel.io.unsync包中的IO類(lèi)。它們能大幅提高你的應用的IO性能。
這里我提供了一個(gè)消除了對Liferay其他類(lèi)文件依賴(lài)的com.liferay.portal.kernel.io.unsync包供大家下載使用。不過(guò)還是推薦大家直接學(xué)習使用Liferay:)