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