出自
[Java中不同的并發實現的性能比較](http://www.importnew.com/14742.html)
[TOC=1,3]
譯文出處:?[花名有孚](http://it.deepinmind.com/%E5%B9%B6%E5%8F%91/2015/01/22/forkjoin-framework-vs-parallel-streams-vs-executorservice-the-ultimate-benchmark.html)???原文出處:[Alex Zhitnitsky](http://blog.takipi.com/forkjoin-framework-vs-parallel-streams-vs-executorservice-the-ultimate-benchmark/)
#### [](http://www.importnew.com/14742.html/forkjoinwars)
## Fork/Join框架在不同配置下的表現如何?
正如即將上映的星球大戰那樣,Java 8的并行流也是毀譽參半。并行流(Parallel Stream)的語法糖就像預告片里的[新型光劍](http://cdn.screenrant.com/wp-content/uploads/Star-Wars-7-The-Force-Awakens-Sith-Lightsaber-Photo.jpg)一樣令人興奮不已。現在Java中實現并發編程存在多種方式,我們希望了解這么做所帶來的性能提升及風險是什么。從經過260多次測試之后拿到的數據來看,還是增加了不少新的見解的,這里我們想和大家分享一下。
## ExecutorService vs. Fork/Join框架 vs. 并行流
在很久很久以前,在一個遙遠的星球上。。好吧,其實我只是想說,在10年前,Java的并發還只能通過第三方庫來實現。然后Java 5到來了,并引入了java.util.concurrent包,上面帶有深深的[Doug Lea](http://g.oswego.edu/)的烙印。ExecutorService為我們提供了一種簡單的操作線程池的方式。當然了,java.util.concurrent包也在不斷完善,Java 7中還引入了基于ExecutorService線程池實現的Fork/Join框架。對很多開發人員來說,Fork/Join框架仍然顯得非常神秘,因此Java 8的stream提供了一種更為方便地使用它的方法。我們來看下這幾種方式有什么不同之處。
我們來通過兩個任務來進行測試,一個是CPU密集型的,一個是IO密集型的,同樣的功能,分別在4種場景下進行測試。不同實現中線程的數量也是一個非常重要的因素,因此這個也是我們測試的目標之一。測試機器共有8個核,因此我們分別使用4,8,16,32個線程來進行測試。對每個任務而言,我們還會測試下單線程的版本,不過這個在圖中并沒有標出來,因為它的時間要長得多。如果想了解這些測試用例是如何運行的,你可以看一下最后的基礎庫一節。我們開始吧。
## 給一段580萬行6GB大小的文本建立索引
在本次測試中我們生成了一個超大的文本文件,并通過相同的方法來建立索引。我們來看下結果如何:
[](http://www.importnew.com/14742.html/file-indexing)
單線程執行時間:176,267毫秒,大約3分鐘。 注意,上圖是從20000毫秒開始的。
### 1\. 線程過少會浪費CPU,而過多則會增加負載
從圖中第一個容易注意到的就是柱狀圖的形狀——光從這4個數據就能大概了解到各個實現的表現是怎樣的了。8個線程到16個線程這里有所傾斜,這是因為某些線程阻塞在了文件IO這里,因此增加線程能更好地使用CPU資源。而當加到32個線程時,由于增加了額外的開銷,性能又開始會變差。
### 2\. 并行流表現最佳。與直接使用Fork/Join相比要快1秒左右
并行流所提供的可不止是語法糖(這里指的并不是lambda表達式),而且它的性能也比Fork/Join框架以及ExecutorService要更好。索引完6GB大小的文件只需要24.33秒。請相信Java,它的性能也能做到很好。
### 3\. 但是。。并行流的表現也是最糟糕的:唯獨它是超過了30秒的
并行流為什么會影響性能,這里也給你上了一課。這在本來就運行著多線程應用的機器上是有可能的。由于可用的線程本身就很少了,直接使用Fork/Join框架要比使用并行流更好一些——兩者的結果相差5秒,大約是18%的性能損耗。
### 4\. 如果涉及到IO操作的話,不要使用默認的線程池大小
測試中使用默認線程池大小(默認值是機器的CPU核數,在這里是8)的并行流,跟使用16個線程相比要慢上2秒。也就是說使用默認的池大小則要慢了7%。這是由于阻塞的IO線程導致的。由于有很多線程處于等待狀態,因此引入更多的線程能夠更好地利用CPU資源,當其它線程在等待調度時不至于讓它們閑著。
如果改變并行流的默認的Fork/Join池的大小?你可以通過一個JVM參數來修改公用的Fork/Join線程池的大小:
`-Djava.util.concurrent.ForkJoinPool.common.parallelism=16`
(默認情況下,所有的Fork/Join任務都會共用同一個線程池,線程的數量等于CPU的核數。好處就是當線程空閑下來時可以收來處理其它任務。)
或者,你還可以用下[這個小技巧](http://blog.krecan.net/2014/03/18/how-to-specify-thread-pool-for-java-8-parallel-streams/),用一個自定義的Fork/Join池來運行并行流。它會覆蓋掉默認的公用的Fork/Join池并讓你能夠使用自己配置好的線程池。手段有點卑劣。測試中我們使用的是公用的線程池。
### 5\. 單線程的性能跟最快的結果相比要慢7.25倍
并發能夠提升7.25倍的性能,考慮到機器是8核的,也就是說接近是8倍的提升!還差的那點應該是消耗在線程的開銷上了。不僅如此,即便是測試中表現最差的并行版本,也就是4個線程的并行流實現(30.23秒),也比單線程的版本(176.27秒)要快5.8倍。
## 如果不考慮IO的話呢?比如判斷某個數是否是素數
對這次測試而言,我們將去除掉IO的部分,來測試下判斷一個大整數是否是素數要花多長時間。這個數有多大?[19位](http://t.qkme.me/3svdwh.jpg),1,530,692,068,127,007,263,換句話說,一百五十三萬零六百九十二兆零六百八十一億兩千萬七千二百六十三。好吧,讓我透透氣先。我們也沒有做任何的優化,而是直接運算到它的平方根,為此我們還檢查了所有的偶數,盡管這個大數并不能被2整除,這只是為了讓運算的時間更久一些。先劇透一下:這的確是一個素數。每個實現運算的次數也都是一樣的。
下面是測試的結果:
[](http://www.importnew.com/14742.html/prime-number)
單線程執行時間:118,127毫秒,大約2分鐘 注意,上圖是從20000毫秒開始的
### 1\. 8個線程與16個線程相差不大
和IO測試中不同,這里并沒有IO調用,因此8個線程和16個線程的差別并不大,Fork/Join的版本例外。由于它的反常表現,我們還多運行了好幾組測試以確保得到的結果是正確的,但事實表明,結果仍是一樣。希望你能在下方的評論一欄說一下你對這個的看法。
### 2\. 不同實現的最好結果都很接近
我們看到,不同的實現版本最快的結果都是一樣的,大約是28秒左右。不管實現的方法如何,結果都大同小異。但這并不意味著使用哪種方法都一樣。請看下面這點。
### 3\. 并行流的線程處理開銷要優于其它實現
這點非常有意思。在本次測試中,我們發現,并行流的16個線程的再次勝出。不止如此,在這次測試中,不管線程數是多少,并行流的表現都是最好的。
### 4\. 單線程的版本比最快的結果要慢4.2倍
除此之外,在運行計算密集型任務時,并行版本的優勢要比帶有IO的測試要減少了2倍。由于這是個CPU密集型的測試,這個結果倒也說得過去,不像前面那個測試中那樣,減少CPU的等待IO的時間能獲得額外的收益。
## 結論
之前我也建議過大家讀一下源碼,了解下[何時應該使用并行流](http://gee.cs.oswego.edu/dl/html/StreamParallelGuidance.html),并且在Java中進行并發編程時,不要武斷地下結論。最好的檢驗方式就是在演示環境中多跑跑類似的測試用例。需要特別注意的因素包括你所運行的硬件環境 (以及測試的硬件環境),還有應用程序的總線程數。包括公用Fork/Join的線程池以及團隊中其它開發人員所寫的代碼中包含的線程。在你編寫自己的并發邏輯前,最好先檢查下上述這些情況,對你的應用程序有一個整體的了解。
### 基礎庫
我們是在EC2的c3.2xlarge實例上運行的本次測試,它有8個vCPU核以及15GB的內存。vCPU是因為這里用到了超線程技術,因此實際上只有4個物理核,但每個核模擬成了兩個。對操作系統的調度器而言,認為我們一共有8個核。為了盡可能的公平,每個實現都運行了10遍,并選擇了第2次到第9次的平均運行時間。也就是一共運行了260次!處理時長也非常重要。我們所選擇的任務的運行時間都會超過20秒,因此時間差異能很容易看出來,而不太受外部因素的影響。
### 最后
原始的測試結果在[這里](https://docs.google.com/a/takipi.com/spreadsheets/d/1yO9WfzNSnREi_T67GLselgr3OqXx0F3ZjhMgJLJrQkM/edit#gid=750297928),代碼放在[Github](https://github.com/takipi/threads-benchmark/tree/master/src/main/java/forkjoin)上。歡迎進行修改,并告訴我們你的測試結果。如果發現了什么我們這里沒有講到的有意思的新的見解或者現象,歡迎告訴我們,我們很希望能把它們追加到本文中。
- JVM
- 深入理解Java內存模型
- 深入理解Java內存模型(一)——基礎
- 深入理解Java內存模型(二)——重排序
- 深入理解Java內存模型(三)——順序一致性
- 深入理解Java內存模型(四)——volatile
- 深入理解Java內存模型(五)——鎖
- 深入理解Java內存模型(六)——final
- 深入理解Java內存模型(七)——總結
- Java內存模型
- Java內存模型2
- 堆內內存還是堆外內存?
- JVM內存配置詳解
- Java內存分配全面淺析
- 深入Java核心 Java內存分配原理精講
- jvm常量池
- JVM調優總結
- JVM調優總結(一)-- 一些概念
- JVM調優總結(二)-一些概念
- VM調優總結(三)-基本垃圾回收算法
- JVM調優總結(四)-垃圾回收面臨的問題
- JVM調優總結(五)-分代垃圾回收詳述1
- JVM調優總結(六)-分代垃圾回收詳述2
- JVM調優總結(七)-典型配置舉例1
- JVM調優總結(八)-典型配置舉例2
- JVM調優總結(九)-新一代的垃圾回收算法
- JVM調優總結(十)-調優方法
- 基礎
- Java 征途:行者的地圖
- Java程序員應該知道的10個面向對象理論
- Java泛型總結
- 序列化與反序列化
- 通過反編譯深入理解Java String及intern
- android 加固防止反編譯-重新打包
- volatile
- 正確使用 Volatile 變量
- 異常
- 深入理解java異常處理機制
- Java異常處理的10個最佳實踐
- Java異常處理手冊和最佳實踐
- Java提高篇——對象克隆(復制)
- Java中如何克隆集合——ArrayList和HashSet深拷貝
- Java中hashCode的作用
- Java提高篇之hashCode
- 常見正則表達式
- 類
- 理解java類加載器以及ClassLoader類
- 深入探討 Java 類加載器
- 類加載器的工作原理
- java反射
- 集合
- HashMap的工作原理
- ConcurrentHashMap之實現細節
- java.util.concurrent 之ConcurrentHashMap 源碼分析
- HashMap的實現原理和底層數據結構
- 線程
- 關于Java并發編程的總結和思考
- 40個Java多線程問題總結
- Java中的多線程你只要看這一篇就夠了
- Java多線程干貨系列(1):Java多線程基礎
- Java非阻塞算法簡介
- Java并發的四種風味:Thread、Executor、ForkJoin和Actor
- Java中不同的并發實現的性能比較
- JAVA CAS原理深度分析
- 多個線程之間共享數據的方式
- Java并發編程
- Java并發編程(1):可重入內置鎖
- Java并發編程(2):線程中斷(含代碼)
- Java并發編程(3):線程掛起、恢復與終止的正確方法(含代碼)
- Java并發編程(4):守護線程與線程阻塞的四種情況
- Java并發編程(5):volatile變量修飾符—意料之外的問題(含代碼)
- Java并發編程(6):Runnable和Thread實現多線程的區別(含代碼)
- Java并發編程(7):使用synchronized獲取互斥鎖的幾點說明
- Java并發編程(8):多線程環境中安全使用集合API(含代碼)
- Java并發編程(9):死鎖(含代碼)
- Java并發編程(10):使用wait/notify/notifyAll實現線程間通信的幾點重要說明
- java并發編程-II
- Java多線程基礎:進程和線程之由來
- Java并發編程:如何創建線程?
- Java并發編程:Thread類的使用
- Java并發編程:synchronized
- Java并發編程:Lock
- Java并發編程:volatile關鍵字解析
- Java并發編程:深入剖析ThreadLocal
- Java并發編程:CountDownLatch、CyclicBarrier和Semaphore
- Java并發編程:線程間協作的兩種方式:wait、notify、notifyAll和Condition
- Synchronized與Lock
- JVM底層又是如何實現synchronized的
- Java synchronized詳解
- synchronized 與 Lock 的那點事
- 深入研究 Java Synchronize 和 Lock 的區別與用法
- JAVA編程中的鎖機制詳解
- Java中的鎖
- TreadLocal
- 深入JDK源碼之ThreadLocal類
- 聊一聊ThreadLocal
- ThreadLocal
- ThreadLocal的內存泄露
- 多線程設計模式
- Java多線程編程中Future模式的詳解
- 原子操作(CAS)
- [譯]Java中Wait、Sleep和Yield方法的區別
- 線程池
- 如何合理地估算線程池大小?
- JAVA線程池中隊列與池大小的關系
- Java四種線程池的使用
- 深入理解Java之線程池
- java并發編程III
- Java 8并發工具包漫游指南
- 聊聊并發
- 聊聊并發(一)——深入分析Volatile的實現原理
- 聊聊并發(二)——Java SE1.6中的Synchronized
- 文件
- 網絡
- index
- 內存文章索引
- 基礎文章索引
- 線程文章索引
- 網絡文章索引
- IOC
- 設計模式文章索引
- 面試
- Java常量池詳解之一道比較蛋疼的面試題
- 近5年133個Java面試問題列表
- Java工程師成神之路
- Java字符串問題Top10
- 設計模式
- Java:單例模式的七種寫法
- Java 利用枚舉實現單例模式
- 常用jar
- HttpClient和HtmlUnit的比較總結
- IO
- NIO
- NIO入門
- 注解
- Java Annotation認知(包括框架圖、詳細介紹、示例說明)