HashMap 非線程安全,多線程環境下會導致數據覆蓋、死循環等嚴重問題。Collections.synchronizedMap 雖安全但粗暴鎖住整個 Map,並發性能極差。ConcurrentHashMap 採用分段鎖設計,將數據分成多個 Segment,每段獨立加鎖,實現了高並發下的優異性能。本文深度解析 JDK 1.7 版本的核心實現。
ConcurrentHashMap
└── Segment[] segments (默認 16 個,2 的 N 次方)
└── Segment 繼承 ReentrantLock (每段就是一把鎖)
└── HashEntry[] table (每段內部是類 HashMap 的數組+鏈表)
└── HashEntry (鏈表節點,key/value/hash/next)
核心思想:鎖分離。操作 Key 時,先定位 Segment,再定位 Segment 內桶位。不同 Segment 互不干擾,理論並發度 = Segment 數量。
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
// 1. 參數校驗與並發級別修正 (MAX_SEGMENTS = 1<<16 = 65536)
if (concurrencyLevel > MAX_SEGMENTS) concurrencyLevel = MAX_SEGMENTS;
// 2. 計算 Segment 數量 (ssize) - 必須是 2 的 N 次方
int sshift = 0, ssize = 1;
while (ssize < concurrencyLevel) { ++sshift; ssize <<= 1; }
this.segmentShift = 32 - sshift; // 用於 hash 高位定位 Segment
this.segmentMask = ssize - 1; // 掩碼,配合位運算定位
// 3. 計算每個 Segment 初始 table 容量
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity) ++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY; // 2
while (cap < c) cap <<= 1;
// 4. 創建 Segment[0] 並發布到 segments 數組
Segment s0 = new Segment<>(loadFactor, (int)(cap * loadFactor), new HashEntry[cap]);
Segment[] ss = (Segment[]) new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // 有序寫入,保證可見性
this.segments = ss;
}
hash & (length-1)) 替代取模,極致性能segments[0] 發布對其他線程可見,無需 volatilepublic V put(K key, V value) {
if (value == null) throw new NullPointerException();
int hash = hash(key);
// 1. 定位 Segment:使用 hash 高位
int j = (hash >>> segmentShift) & segmentMask;
// 2. 獲取或創建 Segment (懶加載)
Segment s;
if ((s = (Segment) UNSAFE.getObjectVolatile(segments, (j << SSHIFT) + SBASE)) == null)
s = ensureSegment(j); // 雙重檢查鎖創建
// 3. 委託給 Segment 處理
return s.put(key, hash, value, false);
}
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 1. 嘗試獲取鎖,失敗則自旋/阻塞獲取
HashEntry node = tryLock() ? null : scanAndLockForPut(key, hash, value);
V oldValue;
try {
// 2. 定位桶位
HashEntry[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry first = entryAt(tab, index);
// 3. 遍歷鏈表查找/插入
for (HashEntry e = first; e != null; e = e.next) {
if (e.hash == hash && key.equals(e.key)) {
oldValue = e.value;
if (!onlyIfAbsent) e.value = value; // 覆蓋值
return oldValue;
}
}
// 4. 頭插法插入新節點 (注意:與 HashMap 尾插不同)
HashEntry newNode = new HashEntry<>(key, hash, first, value);
setEntryAt(tab, index, newNode); // volatile 寫入
++modCount;
++count;
return null;
} finally {
unlock(); // 釋放鎖
}
}
| 特性 | HashMap (JDK 1.7) | ConcurrentHashMap Segment |
|---|---|---|
| 線程安全 | 無 | ReentrantLock 獨佔鎖 |
| 插入方式 | 頭插法 | 頭插法 (同 HashMap 1.7) |
| 擴容觸發 | size > threshold | count > threshold (Segment 級) |
| 擴容鎖 | 無 (單線程) | ReentrantLock (同 Segment 鎖) |
// HashMap.createEntry 非原子操作
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry e = table[bucketIndex]; // 步驟1: 讀取鏈表頭
table[bucketIndex] = new Entry<>(hash, key, value, e); // 步驟2: 覆蓋頭節點
size++;
}
// 線程A、B 同時執行步驟1 獲取同一個 e
// A 完成步驟2: table = A->e
// B 完成步驟2: table = B->e (A 的節點丟失!)
lock()/unlock() 保護下執行volatile 字段 (HashEntry.value, next, table 引用) 保證可見性注意:
get()並非絕對線程安全(無鎖讀),極端情況下可能讀到中間狀態。但得益於 volatile 語義,絕大多數場景足夠安全。追求強一致性需自行加鎖或升級 JDK 1.8+。
public V get(Object key) {
Segment s = segmentFor(hash(key));
return s.get(key, hash);
}
final V get(Object key, int hash) {
if (count != 0) { // 快速失敗
HashEntry e = getFirst(hash);
while (e != null) {
if (e.hash == hash && key.equals(e.key)) {
V v = e.value;
if (v != null) return v; // volatile 讀取
// value 為 null 可能是正在刪除,重新讀取
return readValueUnderLock(e); // 加鎖確認
}
e = e.next;
}
}
return null;
}
| 版本 | 核心機制 | 並發性能 | 適用場景 |
|---|---|---|---|
| JDK 1.7 | 分段鎖 + ReentrantLock + HashEntry 鏈表 | 高 (默認 16 段並發) | 舊系統維護、學習分段鎖思想 |
| JDK 1.8+ | CAS + synchronized + 紅黑樹 + Node 數組 | 更高 (細粒度鎖、無 Segment) | 新項目首選、高並發熱點數據 |
JDK 1.7 ConcurrentHashMap 以分段鎖為核心創新,在保證線程安全的前提下,將鎖粒度細化至 Segment 級別,實現了寫寫互斥、讀寫並行的高並發訪問。理解其 segmentShift/Mask 定位邏輯、scanAndLockForPut 自旋優化、volatile 實現無鎖讀等細節,對掌握 Java 並發編程精髓極具價值。