在 Java 多線程開發中,選擇正確的並發容器至關重要。本文深度解析兩大最常用並發容器 —— ConcurrentHashMap 與 CopyOnWriteArrayList 的實現原理、適用場景、性能特徵與常見誤區。
ConcurrentHashMap:高併發 Map 的首選
JDK 8 核心實現
- 數組 + 鏈表/紅黑樹:同 HashMap,但採用 CAS + synchronized 細粒度鎖。
- 鍵值不可為 null:避免歧義(get 返回 null 可能是不存在或值為 null)。
- 擴容機制:多線程協助擴容,避免全表鎖定。
關鍵操作語義
| 方法 | 線程安全性 | 備註 |
| get(key) | 無鎖,volatile 讀 | 高性能,可能讀到舊值(弱一致性) |
| put(key, val) | CAS + synchronized 鎖頭節點 | 僅鎖定桶,並發度高 |
| computeIfAbsent(k, f) | 原子複合操作 | 推薦替代 putIfAbsent + get |
| size() | 近似值 | 並發下不精確,用 mappingCount() |
| 遍歷 | 弱一致性迭代器 | 不拋 ConcurrentModificationException,反映遍歷時刻快照 |
適用場景
- 高併發讀寫 Map(緩存、計數器、註冊表)。
- 需原子複合操作(如 compute、merge、putIfAbsent)。
- 替代 Hashtable、Collections.synchronizedMap。
常見誤區
- 複合操作非原子:
if (!map.containsKey(k)) map.put(k, v) 非線程安全,必須用 putIfAbsent 或 computeIfAbsent。
- size() 不精確:並發下僅作參考,統計用
mappingCount()(long)。
- 迭代器弱一致:遍歷過程中新增/刪除元素可能不反映、不拋異常。
CopyOnWriteArrayList:讀多寫少的列表神器
核心原理
- 寫時復制:任何修改操作(add/set/remove)都創建新數組副本,舊數組供讀線程繼續使用。
- 讀無鎖:get/iterator 直接訪問當前數組,極快。
- 迭代器快照:構建迭代器時鎖定當前數組,後續修改不影響已有迭代器。
性能特徵
| 操作 | 時間複雜度 | 說明 |
| get/遍歷 | O(1) / O(n) | 無鎖,極快 |
| add/set/remove | O(n) | 需復制整數組,寫入慢 |
| 內存 | 寫入瞬間雙倍 | 大列表頻繁寫入易觸發 GC |
適用場景
- 讀遠多於寫:事件監聽器列表、配置緩存、白名單/黑名單。
- 需要遍歷時不被修改干擾(快照語義)。
- 列表規模較小(< 1000)或寫入極低頻。
不適用場景
- 寫入頻繁或列表巨大(內存/GC 壓力大)。
- 需強一致性讀取最新寫入(讀到的是構建迭代器時的快照)。
- 作為佇列/棧高頻操作。
選型決策表
| 需求 | 推薦容器 | 替代方案 |
| 高併發 Map 讀寫 | ConcurrentHashMap | ConcurrentSkipListMap (有序) |
| 線程安全 List,讀多寫少 | CopyOnWriteArrayList | Collections.synchronizedList + 手動同步遍歷 |
| 高併發佇列 | ConcurrentLinkedQueue | ArrayBlockingQueue (有界) |
| 有序 Map | ConcurrentSkipListMap | TreeMap + 鎖 |
最佳實踐總結
- 默認選 ConcurrentHashMap 替代所有同步 Map 需求。
- 監聽器/觀察者模式列表用 CopyOnWriteArrayList。
- 避免在 ConcurrentHashMap 上做複合邏輯,用原子方法。
- 大列表頻繁寫入改用 ConcurrentLinkedQueue 或加鎖 ArrayList。
- JDK 9+ 可用
Map.ofEntries、List.copyOf 創建不可變集合,天然線程安全。
- 上一篇文章: Spring IOC 容器詳解:BeanFactory 與 ApplicationContext 核心差異
- 下一篇文章: Java 線程基礎入門:創建、啟動、睡眠、合併、讓步完整教程
Copyright ©
程式碼之家 All Rights Reserved