## 工作日記 #004 : 剖析 ConcurrentHashMap 引發的高併發事件 近期在工作中遇到一個生產環境出現的問題,某一台 Server 在高峰期頻繁出現 shutdown 的情況,根據 Grafana 圖表顯示,請求隊列的長度急遽攀升,短時間有 Threads 阻塞、無法被消化的現象產生(見圖右下的 Thread Queue Length):  而從 Server dump 出來的文件多次顯示請求堆積在 ConcurrentHashMap 被 for 迴圈調用的位置: ``` public List<VendorStats> getVendorStats(List<String> merchants) { Set<String> merchantSets = new HashSet<>(merchants); Map<String, VendorStats> sum = new HashMap<>(); for (Map.Entry<String, ConcurrentHashMap<String, AtomicInteger>> entry : gameSummary.entrySet()) { String merchant = entry.getKey(); ConcurrentHashMap<String, AtomicInteger> value = entry.getValue(); if (containsMerchant(merchantSets, merchant)) { for (Map.Entry<String, AtomicInteger> entryValue : value.entrySet()) { String vendor = entryValue.getKey(); if (!sum.containsKey(vendor)) { sum.put(vendor, new VendorStats(vendor)); } sum.get(vendor).getCount().addAndGet(entryValue.getValue().get()); } } } List<VendorStats> returnValue = new ArrayList<>(sum.values()); return getNewSortList(returnValue); } ``` 因此想藉由這個實際案例的剖析來重點學習以下三個主題的知識點: 1. 簡介 ConcurrentHashMap,線程安全跟鎖操作。 3. 原子類型的簡介與用法,何謂 CAS 操作。 4. 淺拷貝與深拷貝差異及用法。 ## 從 ConcurrentHashMap 談線程安全 在討論 ConcurrentHashMap 的特性及底層實現以前,先來簡述為何在這個案例下需要使用該類型的集合,這是因為在線人數統計的情境下,系統會實時呈現出前台用戶的變動狀況,當每一個用戶實際上下線時,後端都需要對數據進行相應的更新,而問題就在於前台用戶在高峰時,可能會有數百數千個用戶同時上下線,每個用戶假設都觸發一個數據更新的統計,那同時就會有非常多條線程需要被處理,此時就會發生高併發事件。 ### 非線程安全 如果此時是用非線程安全的類型,如 HashMap 來操作資料,容易造成數據不一致的情況,原因是: 1. 沒有同步機制來防止 Race Condition (競態條件) 2. 無法保證數據的 Visibility (可見性問題) 3. 可能受到指令 Reordering (重排序問題) 影響 4. 無法保證數據操作的原子性 ### ConcurrentHashMap 如何保證線程安全? ConcurrentHashMap 的底層邏輯在 JDK 1.8 以前是用分段鎖 (Segment) 的方式實現,將整個哈希表分成多個 Segment 進行分段管理。在 1.8 更新後則是改用更細粒度的節點鎖 + CAS (Compare And Swap) 機制來實現,將鎖的粒度進一步細化到了哈希桶層級。兩種實現都利用了精準的同步控制,採用空間換時間的策略,並通過無鎖讀取等優化,在保證高併發環境下線程安全的同時,也能平衡系統執行時的效能。此外,ConcurrentHashMap 始終保持著不允許 null 鍵值、弱一致性迭代等特性,這些設計都是為了在並發環境下提供更好的性能和安全性。 ## 為何淺拷貝可以緩解壓力 即便 ConcurrentHashMap 在底層實作上保證線程安全,但在上述的案例中因為每個線程都需要完整迭代整個 map,加上裡頭又有一個巢狀迴圈,使得每個線程的佔用資源的時間拉長,導致性能開銷加劇,以致於多個線程相互等待與競爭,最後 CPU 資源耗盡,所以可以說真正的始作俑者是 for-each,並非真的是 ConcurrentHashMap 有什麼缺陷。 但這邊使用的淺拷貝方案,剛好可以有效緩解執行壓力,淺拷貝的做法是每個線程在操作集合的數據前,先把共享的資源包在新創建的集合物件中、拷貝一份到本地,爾後操作的資源都是自己獨有的一份,不與其他線程競爭,但事實上內層引用的資源地址還是跟共享的資源一致,所以既可以隔離資源,又能確保修改的數據是正確的。
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up