首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

JDK 1.7 ConcurrentHashMap 源碼深度解析:分段鎖設計與線程安全機制

     編輯:Java 綜合教學

引言:為什麼需要 ConcurrentHashMap?

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;
}

關鍵設計點

  • Segment 數量固定:初始化後不可擴容,避免 Rehash 時的全局鎖競爭
  • 2 的 N 次方:Segment 數量、table 容量均為 2 的冪,利用位運算 (hash & (length-1)) 替代取模,極致性能
  • UNSAFE 有序寫入:保證 segments[0] 發布對其他線程可見,無需 volatile

核心操作:PUT 流程

public 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);
}

Segment.put 實現(加鎖插入)

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 > thresholdcount > threshold (Segment 級)
擴容鎖無 (單線程)ReentrantLock (同 Segment 鎖)

線程安全深度剖析

HashMap 為何不安全?(經典競態)

// 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 的節點丟失!)

ConcurrentHashMap 如何保證安全?

  • 寫操作加鎖:put/remove/replace 等修改操作均在 lock()/unlock() 保護下執行
  • 讀操作無鎖:get 使用 volatile 字段 (HashEntry.value, next, table 引用) 保證可見性
  • 弱一致性迭代器:Iterator 遍歷時不加鎖,反映遍歷瞬間的狀態,不拋出 ConcurrentModificationException

注意:get() 並非絕對線程安全(無鎖讀),極端情況下可能讀到中間狀態。但得益於 volatile 語義,絕大多數場景足夠安全。追求強一致性需自行加鎖或升級 JDK 1.8+。

GET 操作:無鎖讀取

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 並發編程精髓極具價值。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved