Java 7:HashMap与ConcurrentHashMap
本文將重溫這個經典的線程安全問題,并使用一個簡單的Java程序演示與并發線程上下文中涉及的普通舊java.util.HashMap數據結構的錯誤使用有關的風險。
此概念驗證練習將嘗試實現以下三個目標:
- 重新訪問和比較非線程安全和線程安全Map數據結構實現(HashMap,Hashtable,同步的HashMap,ConcurrentHashMap)之間的Java程序性能級別
- 使用每個人都可以編譯,運行和理解的簡單Java程序,復制并演示HashMap無限循環問題
- 回顧上述Map數據結構在現實和現代Java EE容器實現(例如JBoss AS7)中的用法
有關ConcurrentHashMap實現策略的更多詳細信息,我強烈推薦Brian Goetz撰寫的出色文章。
工具和服務器規格
首先,請找到以下用于練習的不同工具和軟件:
- Sun / Oracle JDK和JRE 1.7 64位
- Eclipse Java EE IDE
- Windows Process Explorer(每個Java線程關聯的CPU)
- JVM線程轉儲(阻塞的線程分析和每個線程的CPU相關性)
以下本地計算機用于問題復制過程和性能測量:
- 英特爾(R)酷睿TM i5-2520M CPU @ 2.50Ghz(2個CPU內核,4個邏輯內核)
- 8 GB內存
- Windows 7 64位
* Java程序的結果和性能可能會因您的工作站或服務器規格而異。
Java程序
為了幫助我們實現上述目標,按如下方式創建了一個簡單的Java程序:
- Java主程序是HashMapInfiniteLoopSimulator.java
- 還創建了一個工作線程類WorkerThread.java
該程序正在執行以下操作:
- 初始化大小為2的不同靜態Map數據結構
- 將選定的Map分配給工作線程(您可以在4個Map實現中進行選擇)
- 創建一定數量的工作線程(根據標頭配置)。 為此概念證明創建了3個工作線程NB_THREADS = 3;
- 這些工作線程中的每一個都有相同的任務:使用介于1到1000000之間的隨機 Integer元素查找并在分配的Map數據結構中插入新元素。
- 每個輔助線程執行此任務共計500K次迭代
- 整個程序執行50次迭代,以便為HotSpot JVM提供足夠的啟動時間
- 并發線程上下文是使用JDK ExecutorService實現的
如您所見,Java程序任務相當簡單,但是足夠復雜以生成以下關鍵條件:
- 針對共享/靜態Map數據結構生成并發
- 混合使用get()和put()操作,以嘗試觸發內部鎖和/或內部損壞(對于非線程安全的實現)
- 使用較小的Map初始大小2,強制內部HashMap觸發內部重新哈希/調整大小
最后,可以方便地修改以下參數:
##工作線程數
private static final int NB_THREADS = 3;## Java程序迭代次數
private static final int NB_TEST_ITERATIONS = 50;##地圖數據結構分配。 您可以選擇4種結構
// Plain old HashMap (since JDK 1.2) threadSafeMap1 = new Hashtable<String, Integer>(2);// Plain old Hashtable (since JDK 1.0) threadSafeMap1 = new Hashtable<String, Integer>(2);// Fully synchronized HashMap threadSafeMap2 = new HashMap<String, Integer>(2); threadSafeMap2 = Collections.synchronizedMap(threadSafeMap2);// ConcurrentHashMap (since JDK 1.5) threadSafeMap3 = new ConcurrentHashMap<String, Integer>(2);/*** Assign map at your convenience ****/ assignedMapForTest = threadSafeMap3;現在,在下面找到我們示例程序的源代碼。
#### HashMapInfiniteLoopSimulator.java package org.ph.javaee.training4;import java.util.Collections; import java.util.Map; import java.util.HashMap; import java.util.Hashtable;import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;/*** HashMapInfiniteLoopSimulator* @author Pierre-Hugues Charbonneau**/ public class HashMapInfiniteLoopSimulator {private static final int NB_THREADS = 3;private static final int NB_TEST_ITERATIONS = 50;private static Map<String, Integer> assignedMapForTest = null;private static Map<String, Integer> nonThreadSafeMap = null;private static Map<String, Integer> threadSafeMap1 = null;private static Map<String, Integer> threadSafeMap2 = null;private static Map<String, Integer> threadSafeMap3 = null;/*** Main program* @param args*/public static void main(String[] args) {System.out.println("Infinite Looping HashMap Simulator");System.out.println("Author: Pierre-Hugues Charbonneau");System.out.println("http://javaeesupportpatterns.blogspot.com");for (int i=0; i<NB_TEST_ITERATIONS; i++) {// Plain old HashMap (since JDK 1.2)nonThreadSafeMap = new HashMap<String, Integer>(2);// Plain old Hashtable (since JDK 1.0)threadSafeMap1 = new Hashtable<String, Integer>(2);// Fully synchronized HashMapthreadSafeMap2 = new HashMap<String, Integer>(2);threadSafeMap2 = Collections.synchronizedMap(threadSafeMap2);// ConcurrentHashMap (since JDK 1.5)threadSafeMap3 = new ConcurrentHashMap<String, Integer>(2); // ConcurrentHashMap/*** Assign map at your convenience ****/assignedMapForTest = threadSafeMap3;long timeBefore = System.currentTimeMillis();long timeAfter = 0;Float totalProcessingTime = null;ExecutorService executor = Executors.newFixedThreadPool(NB_THREADS);for (int j = 0; j < NB_THREADS; j++) {/** Assign the Map at your convenience **/Runnable worker = new WorkerThread(assignedMapForTest);executor.execute(worker); }// This will make the executor accept no new threads// and finish all existing threads in the queueexecutor.shutdown();// Wait until all threads are finishwhile (!executor.isTerminated()) {}timeAfter = System.currentTimeMillis();totalProcessingTime = new Float( (float) (timeAfter - timeBefore) / (float) 1000);System.out.println("All threads completed in "+totalProcessingTime+" seconds");}}}#### WorkerThread.java package org.ph.javaee.training4;import java.util.Map;/*** WorkerThread** @author Pierre-Hugues Charbonneau**/ public class WorkerThread implements Runnable {private Map<String, Integer> map = null;public WorkerThread(Map<String, Integer> assignedMap) {this.map = assignedMap;}@Overridepublic void run() {for (int i=0; i<500000; i++) {// Return 2 integers between 1-1000000 inclusiveInteger newInteger1 = (int) Math.ceil(Math.random() * 1000000);Integer newInteger2 = (int) Math.ceil(Math.random() * 1000000); // 1. Attempt to retrieve a random Integer elementInteger retrievedInteger = map.get(String.valueOf(newInteger1));// 2. Attempt to insert a random Integer elementmap.put(String.valueOf(newInteger2), newInteger2); }}}
線程安全的Map實現之間的性能比較
第一個目標是比較使用不同線程安全的Map實現時我們程序的性能水平:
- 普通的舊哈希表(自JDK 1.0起)
- 完全同步的HashMap(通過Collections.synchronizedMap())
- ConcurrentHashMap(自JDK 1.5起)
在下面找到每個迭代的Java程序執行的圖形結果以及程序控制臺輸出示例。
#使用ConcurrentHashMap時的輸出
Infinite Looping HashMap Simulator Author: Pierre-Hugues Charbonneau http://javaeesupportpatterns.blogspot.com All threads completed in 0.984 seconds All threads completed in 0.908 seconds All threads completed in 0.706 seconds All threads completed in 1.068 seconds All threads completed in 0.621 seconds All threads completed in 0.594 seconds All threads completed in 0.569 seconds All threads completed in 0.599 seconds ………………如您所見,ConcurrentHashMap在這里顯然是贏家,所有3個工作線程平均僅花費半秒(在初始啟動后)就可以針對指定的共享Map并在500K循環語句中同時讀取和插入數據。 請注意,程序執行沒有問題,例如沒有掛起情況。
性能的提高肯定是由于ConcurrentHashMap性能的提高,例如無阻塞的get()操作。
其他2個Map實現的性能水平非常相似,但對于同步的HashMap而言卻具有很小的優勢。
HashMap無限循環問題復制
下一個目標是復制從Java EE生產環境中經常觀察到的HashMap無限循環問題。 為此,您只需要按照下面的代碼片段分配非線程安全的HashMap實現即可:
/*** Assign map at your convenience ****/ assignedMapForTest = nonThreadSafeMap;使用非線程安全的HashMap按原樣運行程序應導致:
- 除程序頭外無輸出
- 從系統觀察到的CPU大量增加
- Java程序有時會掛起,您將被迫殺死Java進程
發生了什么? 為了了解這種情況并確認問題,我們將使用Process Explorer和JVM Thread Dump從Windows操作系統執行每個線程的CPU分析。
1 –再次運行程序,然后按照以下方法從Process Explorer快速捕獲每個CPU數據的線程。 在explorer.exe下,您需要右鍵單擊javaw.exe并選擇屬性。 將顯示“線程”選項卡。 我們可以看到幾乎所有系統CPU都使用了4個線程。
2 –現在,您必須使用JDK 1.7 jstack實用程序快速捕獲JVM線程轉儲。 對于我們的示例,我們可以看到我們的3個工作線程,它們似乎忙/忙于執行get()和put()操作。
..\jdk1.7.0\bin>jstack 272 2012-08-29 14:07:26 Full thread dump Java HotSpot(TM) 64-Bit Server VM (21.0-b17 mixed mode):"pool-1-thread-3" prio=6 tid=0x0000000006a3c000 nid=0x18a0 runnable [0x0000000007ebe000]java.lang.Thread.State: RUNNABLEat java.util.HashMap.put(Unknown Source)at org.ph.javaee.training4.WorkerThread.run(WorkerThread.java:32)at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)at java.lang.Thread.run(Unknown Source)"pool-1-thread-2" prio=6 tid=0x0000000006a3b800 nid=0x6d4 runnable [0x000000000805f000]java.lang.Thread.State: RUNNABLEat java.util.HashMap.get(Unknown Source)at org.ph.javaee.training4.WorkerThread.run(WorkerThread.java:29)at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)at java.lang.Thread.run(Unknown Source)"pool-1-thread-1" prio=6 tid=0x0000000006a3a800 nid=0x2bc runnable [0x0000000007d9e000]java.lang.Thread.State: RUNNABLEat java.util.HashMap.put(Unknown Source)at org.ph.javaee.training4.WorkerThread.run(WorkerThread.java:32)at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)at java.lang.Thread.run(Unknown Source) ..............現在該按照以下方法將Process Explorer線程ID DECIMAL格式轉換為HEXA格式。 HEXA值使我們可以按照以下方式映射和標識每個線程:
## TID:1748(nid = 0X6D4)
- 線程名稱:pool-1-thread-2
- CPU @ 25.71%
- 任務:工作線程執行HashMap.get()操作
## TID:700(nid = 0X2BC)
- 線程名稱:pool-1-thread-1
- CPU @ 23.55%
- 任務:工作線程執行HashMap.put()操作
## TID:6304(nid = 0X18A0)
- 線程名稱:pool-1-thread-3
- CPU @ 12.02%
- 任務:工作線程執行HashMap.put()操作
## TID:5944(nid = 0X1738)
- 線程名稱:pool-1-thread-1
- CPU @ 20.88%
- 任務:主Java程序執行
如您所見,上面的相關性和分析非常有啟發性。 我們的主要Java程序處于掛起狀態,因為我們的3個工作線程正在占用大量CPU,并且無法正常運行。 它們在執行HashMap get()和put()時可能看起來“卡住”,但實際上它們都涉及無限循環條件。 這正是我們想要復制的內容。
HashMap無限循環深入探究
現在,讓我們進一步分析,以更好地了解這種循環條件。 為此,我們在JDK 1.7 HashMap Java類本身中添加了跟蹤代碼,以了解正在發生的情況。 為put()操作添加了類似的日志記錄,還添加了一條跟蹤,指示內部和自動重新哈希/調整大小已觸發。
在get()和put()操作中添加的跟蹤使我們能夠確定for()循環是否正在處理循環依賴關系,這將解釋無限循環條件。
再次,添加的日志記錄非常有啟發性。 我們可以看到,在幾個內部HashMap.resize()之后,內部結構受到了影響,創建了循環依賴條件,并觸發了這個無限循環條件(#iterations不斷增加和增加……)而沒有退出條件。
這也表明resize()/ rehash操作最容易遭受內部損壞,尤其是當使用默認的HashMap大小16時。這意味著HashMap的初始大小似乎是造成風險的重要因素。問題復制。
最后,有趣的是,我們能夠通過將初始大小設置為1000000來成功運行非線程安全HashMap的測試用例,從而完全避免了任何調整大小。 在合并圖結果下方找到:
HashMap是我們表現最好的,但是僅在防止內部調整大小時才使用。 同樣,這絕對不是解決線程安全風險的方法,而只是一種方法,表明考慮到當時執行的HashMap的整個操作,調整大小操作的風險最大。
到目前為止,ConcurrentHashMap是我們的整體贏家,因為它針對該測試用例提供了快速的性能和線程安全性。
JBoss AS7 Map數據結構用法
現在,我們將通過研究現代Java EE容器實現(例如JBoss AS 7.1.2)中的不同Map實現來結束本文。 您可以從github master分支獲取最新的源代碼。
在報告下方找到:
- JBoss AS7.1.2 Java文件總數(2012年8月28日快照):7302
- 使用java.util.Hashtable的Java類總數:72
- 使用java.util.HashMap的Java類總數:512
- 使用同步的HashMap的Java類總數:18
- 使用ConcurrentHashMap的Java類總數:46
哈希表引用主要在測試套件組件中以及命名和與JNDI相關的實現中找到。 這種低使用率在這里不足為奇。
從512個Java類中找到了對java.util.HashMap的引用。 考慮到自從最近幾年以來這種實現方式的普及程度,這再次不足為奇。 但是,重要的是要提到,從局部變量(未在線程間共享),同步的HashMap或手動同步防護措施中找到了很好的比率,因此“技術上”使線程安全,并且不會暴露于上述無限循環條件(待處理/隱藏的錯誤)考慮到Java并發編程的復雜性,這仍然是一個現實……涉及Oracle Service Bus 11g的案例研究就是一個很好的例子)。
發現JMS,EJB3,RMI和集群等軟件包中只有18個Java類,使用的同步HashMap使用率較低。
最后,在下面找到ConcurrentHashMap用法的細分,這是我們主要的興趣所在。 正如您將在下面看到的那樣,關鍵的JBoss組件層(例如Web容器,EJB3實現等)使用此Map實現。
## JBoss單點登錄
用于管理涉及并發線程訪問的內部SSO ID
合計:1
## JBoss Java EE和Web容器
這并不奇怪,因為許多內部Map數據結構用于管理http會話對象,
部署注冊表,群集和復制,統計信息等,并發線程訪問量大。 總數:11
## JBoss JNDI和安全層
由高度并發的結構(例如內部JNDI安全管理)使用。
合計:4
## JBoss域和受管服務器管理,推出計劃…
合計:7
## JBoss EJB3
由數據結構使用,例如文件計時器持久性存儲,應用程序異常,實體Bean緩存,序列化,鈍化…
合計:8
## JBoss內核,線程池和協議管理
由高并發線程數映射數據結構使用,這些數據結構涉及處理和分派/處理傳入請求(例如HTTP)。
合計:3
## JBoss連接器,例如JDBC / XA DataSources…
合計:2
## Weld(JSR-299的參考實現:JavaTM EE平臺的上下文和依賴注入)用于ClassLoader和涉及并發線程訪問的并發靜態Map數據結構的上下文。
合計:3
## JBoss測試套件用于某些集成測試用例,例如內部數據存儲,ClassLoader測試等。
合計:3
最后的話
我希望本文能幫助您重新研究這個經典問題,并理解與錯誤使用非線程安全HashMap實現有關的常見問題和風險之一。 我的主要建議是在并發線程上下文中使用HashMap時要小心。 除非您是Java并發專家,否則我建議您改用ConcurrentHashMap,它在性能和線程安全性之間提供了很好的平衡。
像往常一樣,總是建議進行額外的盡職調查,例如執行負載和性能測試周期。 這將使您能夠在將解決方案推廣到客戶生產環境之前檢測線程安全和/或性能問題。
參考: Java 7:我們的JCG合作伙伴 Pierre-Hugues Charbonneau的HashMap與ConcurrentHashMap ,位于Java EE支持模式和Java教程博客。
翻譯自: https://www.javacodegeeks.com/2012/08/java-7-hashmap-vs-concurrenthashmap.html
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的Java 7:HashMap与ConcurrentHashMap的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring事件的观察者模式
- 下一篇: 莫斯科国立大学推出新型超级计算机,峰值算