---
# System prepended metadata

title: '工作日記 #004 : 剖析 ConcurrentHashMap 引發的高併發事件'
tags: [Worklog, Java]

---

## 工作日記 #004 : 剖析 ConcurrentHashMap 引發的高併發事件
近期在工作中遇到一個生產環境出現的問題，某一台 Server 在高峰期頻繁出現 shutdown 的情況，根據 Grafana 圖表顯示，請求隊列的長度急遽攀升，短時間有 Threads 阻塞、無法被消化的現象產生（見圖右下的 Thread Queue Length）：
![Pasted Graphic](https://hackmd.io/_uploads/SJbKBwwuyl.png)
而從 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 有什麼缺陷。

但這邊使用的淺拷貝方案，剛好可以有效緩解執行壓力，淺拷貝的做法是每個線程在操作集合的數據前，先把共享的資源包在新創建的集合物件中、拷貝一份到本地，爾後操作的資源都是自己獨有的一份，不與其他線程競爭，但事實上內層引用的資源地址還是跟共享的資源一致，所以既可以隔離資源，又能確保修改的數據是正確的。