本期Blog原文參見(jiàn):
http://www.liferay.com/web/shuyang.zhou/blog/-/blogs/io-performance
IO操作幾乎對(duì)于所有的應(yīng)用都是非常重要的,因?yàn)镮O操作非常容易導(dǎo)致性能瓶頸。
在Java的世界里存在兩大類IO,傳統(tǒng)IO(TIO)和新IO(NIO)。外加一個(gè)即將到來(lái)的增強(qiáng)版的NIO——NIO2(JDK7)。
NIO(以及NIO2)主要用于在一些特定情況下增強(qiáng)性能、提供更好的操作系統(tǒng)層次IO功能集成,但它們無(wú)法完全替代TIO!在許多情況下TIO仍然是你唯一的選擇。
今天我們就來(lái)討論一下TIO的性能問(wèn)題。
IO的性能瓶頸主要分為兩類:
- 錯(cuò)誤的使用緩沖(buffer)
- 過(guò)度的同步保護(hù)
我們都知道buffer能夠增加IO的性能,但不是每個(gè)人都知道如何正確的使用buffer,在本文結(jié)束時(shí)我會(huì)給出一些最佳實(shí)踐建議。
第一部分:
對(duì)于1,錯(cuò)誤的使用緩沖(buffer),存在兩點(diǎn)非常流行的錯(cuò)誤用法和一個(gè)感念上的誤解
- a)為內(nèi)存IO類(In-memory IO class)添加緩沖(錯(cuò)誤用法)
- b)為已添加buffer的IO類再次添加buffer(錯(cuò)誤用法)
- c)Buffer版的IO類和顯式使用buffer間的關(guān)系(概念上的誤解)
對(duì)于a),這是非常荒謬的!添加buffer的目的是為了將對(duì)IO設(shè)備的多次訪問(wèn)合并為一次訪問(wèn)從而提高性能,In-memory類(如ByteArrayInput/OutputStream)根本就不會(huì)訪問(wèn)任何IO設(shè)備,所以對(duì)它們添加buffer是完全沒(méi)有必要的。
對(duì)于b),這是完全多余的!你只需要buffer一次就可以了,多于一次的buffer只會(huì)引入更多的棧調(diào)用和垃圾創(chuàng)建。
對(duì)于c),這需要多一點(diǎn)解釋:
從本質(zhì)上講,這兩種做法是要達(dá)到相同的目的,但方法不同,這也導(dǎo)致了它們之間具有巨大的性能差異!
針對(duì)這一問(wèn)題我做了一個(gè)測(cè)試,比較使用Buffer版的IO類和顯式使用buffer在讀/寫(xiě)文件時(shí)的性能差異。
下面的測(cè)試結(jié)果顯式了兩者的性能差異:
讀測(cè)試結(jié)果:(所有數(shù)據(jù)都是在JVM預(yù)熱后取得的,每個(gè)采樣點(diǎn)的時(shí)間是10次讀取操作的總時(shí)間,單位為毫秒)
文件大小 |
1K |
10K
|
100K
|
1M
|
10M
|
100M
|
1G
|
BufferedInputStream |
0 |
1
|
5 |
53 |
549 |
5492 |
56002 |
顯式使用byte[]在FileInputStream上讀取 |
0 |
0 |
1
|
10
|
113
|
1126
|
11448
|
寫(xiě)測(cè)試結(jié)果:(所有數(shù)據(jù)都是在JVM預(yù)熱后取得的,每個(gè)采樣點(diǎn)的時(shí)間是10次寫(xiě)入操作的總時(shí)間,單位為毫秒)
文件大小 |
1K |
10K
|
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類導(dǎo)致很多無(wú)謂的棧調(diào)用(都是裝飾者模式惹得禍decorator pattern)
-
JDK中所有的Buffer版IO類都是線程安全的,這就意味著它們添加了大量的同步保護(hù)(將在第二部分中詳細(xì)解釋)
現(xiàn)在你知道了顯式使用buffer要比使用Buffer版的IO類具有更好的性能,所以請(qǐng)盡量多顯式使用buffer,但在兩種特殊情況下你仍然需要Buffer版的IO類:
-
當(dāng)你在使用第三方庫(kù)時(shí),庫(kù)的api需要IO類作為參數(shù),并且你確定他們內(nèi)部的代碼采用流式方式編碼(非塊式操作),也就是沒(méi)有顯式使用buffer。在不修改他們代碼的前提下,你只能通過(guò)傳入Buffer版的IO類對(duì)象來(lái)提升性能。
-
另外一種情況就是,如果你很懶,你會(huì)更傾向于使用Buffer版的IO類,因?yàn)樗鼈兡芄?jié)省你幾行代碼(相對(duì)于顯式使用buffer)。
第二部分:
對(duì)于2,過(guò)度的同步保護(hù),我指的是JDK的IO包,也就是java.io包。
我一點(diǎn)也不喜歡這個(gè)包里面的代碼,因?yàn)樗麄兌际蔷€程安全的,也就意味著許多同步保護(hù)。如果我需要同步保護(hù),我會(huì)自己去做,而且我絕不會(huì)去添加任何多余的保護(hù)。但在這一點(diǎn)上JDK的IO包把我逼得無(wú)路可走
只要你使用JDK的IO包,你就被迫的添加了許多同步保護(hù),即使你完全確定你的代碼運(yùn)行在單一線程的環(huán)境下,你也不能回避這些不必要的保護(hù)。你也許會(huì)好奇的問(wèn),這真的是一個(gè)嚴(yán)重的問(wèn)題嗎?JVM在運(yùn)行時(shí)會(huì)對(duì)弱競(jìng)爭(zhēng)的鎖進(jìn)行優(yōu)化,不是嗎?顯然,它做的優(yōu)化還不到家,讓我們來(lái)看一下性能測(cè)試結(jié)果。
我翻版了一批JDK IO包中常用的類,這個(gè)翻版完全是API層次的翻版,也就是說(shuō)全部代碼是參照J(rèn)DK IO包的Javadoc寫(xiě)成, 沒(méi)有直接借用JDK的源碼。因?yàn)镴DK源碼大部分使用GPL協(xié)議發(fā)布,而Liferay的代碼采用MIT協(xié)議發(fā)布,為了不引起IP糾紛只好照葫蘆畫(huà)瓢。而實(shí)際上從0開(kāi)始創(chuàng)建這些類一點(diǎn)也不難(僅僅是裝飾者模式而已),只是非常的繁瑣。在我的翻版類中,我移除了全部的同步保護(hù)。而我的測(cè)試也進(jìn)行在單一線程環(huán)境下,所以不用擔(dān)心線程安全的問(wèn)題。
測(cè)試包含兩部分,第一部分比較原始JDK IO類和我的unsyc版的IO類在讀取內(nèi)存數(shù)據(jù)(In-memory data)時(shí)的性能差異,第二部分比較原始JDK IO類和我的unsyc版的IO類在寫(xiě)入內(nèi)存數(shù)據(jù)(In-memory data)時(shí)的性能差異。之所以采用內(nèi)存數(shù)據(jù)而不是磁盤(pán)數(shù)據(jù)是為了放大同步操作對(duì)整體性能的影響,以便于分析。
讀測(cè)試結(jié)果:
寫(xiě)測(cè)試結(jié)果:

寫(xiě)數(shù)據(jù)的測(cè)試曲線不像讀的那樣平滑,原因在于它內(nèi)部使用了一個(gè)動(dòng)態(tài)增長(zhǎng)的byte[],這導(dǎo)致大量GC活動(dòng)(與上一期Blog中我們討論SB時(shí)看到的問(wèn)題相似)。
好了,現(xiàn)在你應(yīng)該看到了同步保護(hù)是一項(xiàng)多么沉重的操作。我們?nèi)粘i_(kāi)發(fā)中存在大量局限在方法調(diào)用棧內(nèi)的IO類使用,這些情況都是絕對(duì)發(fā)生在單一線程環(huán)境下的。另外一些時(shí)候,即使IO對(duì)象的引用超出了方法調(diào)用棧的作用域,但我們可以通過(guò)分析得知它仍然只會(huì)被單一線程所訪問(wèn),比如web開(kāi)發(fā)中,針對(duì)一個(gè)request的全部處理一般都是由一個(gè)worker thread來(lái)完成的(除非你的后臺(tái)還有其他的異步服務(wù)線程與worker間交換數(shù)據(jù),但這很少見(jiàn))。對(duì)于這樣的情況,你大可以放心的使用這些unsyc的IO類(com.liferay.portal.kernel.io.unsync)。
更多關(guān)于unsycIO類的信息請(qǐng)查看Liferay的JIRA鏈接:
http://issues.liferay.com/browse/LPS-6648
最后給大家留下一些建議:
-
如果可能請(qǐng)盡量顯式使用buffer,而不是使用Buffer版的IO類。
-
僅在使用第三方類庫(kù)和你很懶的時(shí)候使用Buffer版的IO類。
-
當(dāng)你確定你的代碼運(yùn)行在單一線程環(huán)境下,或者你自己添加了同步保護(hù)時(shí),請(qǐng)使用com.liferay.portal.kernel.io.unsync包中的IO類。它們能大幅提高你的應(yīng)用的IO性能。
這里我提供了一個(gè)消除了對(duì)Liferay其他類文件依賴的com.liferay.portal.kernel.io.unsync包供大家下載使用。不過(guò)還是推薦大家直接學(xué)習(xí)使用Liferay:)
http://www.tkk7.com/Files/ShuyangZhou/IOPerformance/src.zip