Fork/Join框架在不同配置下的表現(xiàn)如何?
正如電影星球大戰(zhàn)那樣,Java 8的并行流也是毀譽(yù)參半。并行流(Parallel Stream)的語法糖就像預(yù)告片里的新型光劍一樣令人興奮不已。現(xiàn)在Java中實(shí)現(xiàn)并發(fā)編程存在多種方式,我們希望了解這么做所帶來的性能提升及風(fēng)險是什么。從經(jīng)過260多次測試之后拿到的數(shù)據(jù)來看,還是增加了不少新的見解的,這里我們想和大家分享一下。
歡迎工作一到五年的Java工程師朋友們加入Java技術(shù)交流:611481448
群內(nèi)提供免費(fèi)的Java架構(gòu)學(xué)習(xí)資料(里面有高可用、高并發(fā)、高性能及分布式、Jvm性能調(diào)優(yōu)、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點(diǎn)的架構(gòu)資料)合理利用自己每一分每一秒的時間來學(xué)習(xí)提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!
ExecutorService vs. Fork/Join框架 vs. 并行流
在很久很久以前,在一個遙遠(yuǎn)的星球上。。好吧,其實(shí)我只是想說,在10年前,Java的并發(fā)還只能通過第三方庫來實(shí)現(xiàn)。然后Java 5到來了,并引入了java.util.concurrent包,上面帶有深深的Doug Lea的烙印。ExecutorService為我們提供了一種簡單的操作線程池的方式。當(dāng)然了,java.util.concurrent包也在不斷完善,Java 7中還引入了基于ExecutorService線程池實(shí)現(xiàn)的Fork/Join框架。對很多開發(fā)人員來說,F(xiàn)ork/Join框架仍然顯得非常神秘,因此Java 8的stream提供了一種更為方便地使用它的方法。我們來看下這幾種方式有什么不同之處。
我們來通過兩個任務(wù)來進(jìn)行測試,一個是CPU密集型的,一個是IO密集型的,同樣的功能,分別在4種場景下進(jìn)行測試。不同實(shí)現(xiàn)中線程的數(shù)量也是一個非常重要的因素,因此這個也是我們測試的目標(biāo)之一。測試機(jī)器共有8個核,因此我們分別使用4,8,16,32個線程來進(jìn)行測試。對每個任務(wù)而言,我們還會測試下單線程的版本,不過這個在圖中并沒有標(biāo)出來,因?yàn)樗臅r間要長得多。如果想了解這些測試用例是如何運(yùn)行的,你可以看一下最后的基礎(chǔ)庫一節(jié)。我們開始吧。
給一段580萬行6GB大小的文本建立索引
在本次測試中我們生成了一個超大的文本文件,并通過相同的方法來建立索引。我們來看下結(jié)果如何:
單線程執(zhí)行時間:176,267毫秒,大約3分鐘。 注意,上圖是從20000毫秒開始的。
1. 線程過少會浪費(fèi)CPU,而過多則會增加負(fù)載
從圖中第一個容易注意到的就是柱狀圖的形狀——光從這4個數(shù)據(jù)就能大概了解到各個實(shí)現(xiàn)的表現(xiàn)是怎樣的了。8個線程到16個線程這里有所傾斜,這是因?yàn)槟承┚€程阻塞在了文件IO這里,因此增加線程能更好地使用CPU資源。而當(dāng)加到32個線程時,由于增加了額外的開銷,性能又開始會變差。
2. 并行流表現(xiàn)最佳。與直接使用Fork/Join相比要快1秒左右
并行流所提供的可不止是語法糖(這里指的并不是lambda表達(dá)式),而且它的性能也比Fork/Join框架以及ExecutorService要更好。索引完6GB大小的文件只需要24.33秒。請相信Java,它的性能也能做到很好。
3. 但是。。并行流的表現(xiàn)也是最糟糕的:唯獨(dú)它是超過了30秒的
并行流為什么會影響性能,這里也給你上了一課。這在本來就運(yùn)行著多線程應(yīng)用的機(jī)器上是有可能的。由于可用的線程本身就很少了,直接使用Fork/Join框架要比使用并行流更好一些——兩者的結(jié)果相差5秒,大約是18%的性能損耗。
4. 如果涉及到IO操作的話,不要使用默認(rèn)的線程池大小
測試中使用默認(rèn)線程池大?。J(rèn)值是機(jī)器的CPU核數(shù),在這里是8)的并行流,跟使用16個線程相比要慢上2秒。也就是說使用默認(rèn)的池大小則要慢了7%。這是由于阻塞的IO線程導(dǎo)致的。由于有很多線程處于等待狀態(tài),因此引入更多的線程能夠更好地利用CPU資源,當(dāng)其它線程在等待調(diào)度時不至于讓它們閑著。
如果改變并行流的默認(rèn)的Fork/Join池的大?。磕憧梢酝ㄟ^一個JVM參數(shù)來修改公用的Fork/Join線程池的大?。?/p>
-Djava.util.concurrent.ForkJoinPool.common.parallelism=16
(默認(rèn)情況下,所有的Fork/Join任務(wù)都會共用同一個線程池,線程的數(shù)量等于CPU的核數(shù)。好處就是當(dāng)線程空閑下來時可以收來處理其它任務(wù)。)
或者,你還可以用下這個小技巧,用一個自定義的Fork/Join池來運(yùn)行并行流。它會覆蓋掉默認(rèn)的公用的Fork/Join池并讓你能夠使用自己配置好的線程池。手段有點(diǎn)卑劣。測試中我們使用的是公用的線程池。
5. 單線程的性能跟最快的結(jié)果相比要慢7.25倍
并發(fā)能夠提升7.25倍的性能,考慮到機(jī)器是8核的,也就是說接近是8倍的提升!還差的那點(diǎn)應(yīng)該是消耗在線程的開銷上了。不僅如此,即便是測試中表現(xiàn)最差的并行版本,也就是4個線程的并行流實(shí)現(xiàn)(30.23秒),也比單線程的版本(176.27秒)要快5.8倍。
如果不考慮IO的話呢?比如判斷某個數(shù)是否是素?cái)?shù)
對這次測試而言,我們將去除掉IO的部分,來測試下判斷一個大整數(shù)是否是素?cái)?shù)要花多長時間。這個數(shù)有多大?19位,1,530,692,068,127,007,263,換句話說,一百五十三萬零六百九十二兆零六百八十一億兩千萬七千二百六十三。好吧,讓我透透氣先。我們也沒有做任何的優(yōu)化,而是直接運(yùn)算到它的平方根,為此我們還檢查了所有的偶數(shù),盡管這個大數(shù)并不能被2整除,這只是為了讓運(yùn)算的時間更久一些。先劇透一下:這的確是一個素?cái)?shù)。每個實(shí)現(xiàn)運(yùn)算的次數(shù)也都是一樣的。
下面是測試的結(jié)果:
單線程執(zhí)行時間:118,127毫秒,大約2分鐘 注意,上圖是從20000毫秒開始的
1. 8個線程與16個線程相差不大
和IO測試中不同,這里并沒有IO調(diào)用,因此8個線程和16個線程的差別并不大,F(xiàn)ork/Join的版本例外。由于它的反常表現(xiàn),我們還多運(yùn)行了好幾組測試以確保得到的結(jié)果是正確的,但事實(shí)表明,結(jié)果仍是一樣。希望你能在下方的評論一欄說一下你對這個的看法。
2. 不同實(shí)現(xiàn)的最好結(jié)果都很接近
我們看到,不同的實(shí)現(xiàn)版本最快的結(jié)果都是一樣的,大約是28秒左右。不管實(shí)現(xiàn)的方法如何,結(jié)果都大同小異。但這并不意味著使用哪種方法都一樣。請看下面這點(diǎn)。
3. 并行流的線程處理開銷要優(yōu)于其它實(shí)現(xiàn)
這點(diǎn)非常有意思。在本次測試中,我們發(fā)現(xiàn),并行流的16個線程的再次勝出。不止如此,在這次測試中,不管線程數(shù)是多少,并行流的表現(xiàn)都是最好的。
4. 單線程的版本比最快的結(jié)果要慢4.2倍
除此之外,在運(yùn)行計(jì)算密集型任務(wù)時,并行版本的優(yōu)勢要比帶有IO的測試要減少了2倍。由于這是個CPU密集型的測試,這個結(jié)果倒也說得過去,不像前面那個測試中那樣,減少CPU的等待IO的時間能獲得額外的收益。
結(jié)論
之前我也建議過大家讀一下源碼,了解下何時應(yīng)該使用并行流,并且在Java中進(jìn)行并發(fā)編程時,不要武斷地下結(jié)論。最好的檢驗(yàn)方式就是在演示環(huán)境中多跑跑類似的測試用例。需要特別注意的因素包括你所運(yùn)行的硬件環(huán)境 (以及測試的硬件環(huán)境),還有應(yīng)用程序的總線程數(shù)。包括公用Fork/Join的線程池以及團(tuán)隊(duì)中其它開發(fā)人員所寫的代碼中包含的線程。在你編寫自己的并發(fā)邏輯前,最好先檢查下上述這些情況,對你的應(yīng)用程序有一個整體的了解。
基礎(chǔ)庫
我們是在EC2的c3.2xlarge實(shí)例上運(yùn)行的本次測試,它有8個vCPU核以及15GB的內(nèi)存。vCPU是因?yàn)檫@里用到了超線程技術(shù),因此實(shí)際上只有4個物理核,但每個核模擬成了兩個。對操作系統(tǒng)的調(diào)度器而言,認(rèn)為我們一共有8個核。為了盡可能的公平,每個實(shí)現(xiàn)都運(yùn)行了10遍,并選擇了第2次到第9次的平均運(yùn)行時間。也就是一共運(yùn)行了260次!處理時長也非常重要。我們所選擇的任務(wù)的運(yùn)行時間都會超過20秒,因此時間差異能很容易看出來,而不太受外部因素的影響。
喜歡小編輕輕點(diǎn)個關(guān)注吧!