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

Java 並發容器深度解析:同步容器 vs 並發容器、ConcurrentHashMap 核心原理與鎖分段技術

     編輯:Java 程式設計入門

同步容器 vs 並發容器:核心差異

維度同步容器並發容器
代表類別Vector、Hashtable、Collections.synchronizedXxxConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue
實現方式方法級 synchronized,鎖住整個物件分段鎖、CAS、複製於寫、無鎖算法
併發效能串行執行,擴展性差高併發支援,擴展性強
複合操作需額外客戶端鎖(如 getLast)內建原子複合操作(putIfAbsent 等)
迭代器快速失敗,需外部同步弱一致性,無需鎖定

同步容器隱患:複合操作非原子

// 看似安全,實則有競態條件
public static Object getLast(Vector<Object> list) {
    int lastIndex = list.size() - 1;      // 操作 1
    return list.get(lastIndex);           // 操作 2
}

public static void deleteLast(Vector<Object> list) {
    int lastIndex = list.size() - 1;
    list.remove(lastIndex);
}

// 多執行緒同時調用:執行緒 A 獲取索引,執行緒 B 刪除元素,A 再 get → IndexOutOfBoundsException
// 解決:客戶端鎖定 list 物件
synchronized(list) { ... }

ConcurrentHashMap 核心架構(JDK 1.8+)

┌─────────────────────────────────────────────────────────┐
│                    ConcurrentHashMap                     │
├─────────────────────────────────────────────────────────┤
│  Node<K,V>[] table (初始容量 16,2的冪次)                │
│  - 每個桶頭節點獨立鎖定(synchronized/CAS)             │
│  - 擴容支援多執行緒協作遷移                              │
└─────────────────────────────────────────────────────────┘

JDK 1.7 vs 1.8 關鍵演進

特性JDK 1.7 (分段鎖)JDK 1.8 (CAS + synchronized)
結構Segment[] + HashEntry[]Node[] + TreeBin
鎖粒度16 個 Segment(預設)桶級別(每個桶頭節點)
Put 操作鎖 SegmentCAS 失敗則 synchronized 鎖桶頭
Get 操作無鎖(volatile)無鎖(volatile + 不變性)
擴容單執行緒鎖定 Segment多執行緒協作遷移
樹化不支援鏈表 ≥ 8 且容量 ≥ 64 轉紅黑樹

核心操作深度解析

Get 操作:完全無鎖

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        if ((eh = e.hash) == h) { // 直接命中桶頭
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        } else if (eh < 0) { // 樹節點或轉移節點
            return (e instanceof TreeBin) ? ((TreeBin<K,V>)e).get(h, key) : e.get(h, key);
        }
        // 鏈表遍歷
        while ((e = e.next) != null) {
            if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}
  • 利用 volatile 保證可見性
  • Node 結構不可變(val/next 為 volatile),遍歷過程安全
  • 無需加鎖,極高讀效能

Put 操作:CAS + synchronized 桶鎖

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int h = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable(); // 延遲初始化
        else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {
            // 1. 桶為空,CAS 嘗試插入
            if (casTabAt(tab, i, null, new Node<>(h, key, value, null)))
                break; // 成功
        } else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f); // 協助擴容
        else {
            // 2. 桶非空,鎖定桶頭節點
            synchronized (f) {
                if (tabAt(tab, i) == f) { // 雙重檢查
                    if (fh >= 0) { // 鏈表
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
                                if (!onlyIfAbsent) e.val = value;
                                return e.val;
                            }
                            if (e.next == null) {
                                e.next = new Node<>(h, key, value, null);
                                break;
                            }
                            e = e.next;
                        }
                    } else if (f instanceof TreeBin) { // 紅黑樹
                        Node<K,V> p = ((TreeBin<K,V>)f).putTreeVal(h, key, value);
                        if (p != null) return p.val;
                    }
                }
            }
            // 3. 達閾值樹化
            if (binCount != 0 && binCount >= TREEIFY_THRESHOLD)
                treeifyBin(tab, i);
        }
    }
    addCount(1, binCount); // 更新 size,可能觸發擴容
    return null;
}

Size 計算:LongAdder 思想

  • baseCount + CounterCell[] 分散計數
  • 低併發更新 baseCount,高併發分散到 CounterCell
  • size() 返回近似值,非精確實時值

Remove 操作與弱一致性

刪除不銷毀節點,構建新鏈表

// 刪除節點 C:A → B → C → D
// 新鏈表:A' → B' → D (C 及之前節點複製,C 之後共享)
  • 讀執行緒遍歷舊鏈表不受影響(不可變性)
  • 寫執行緒構建新鏈表,CAS 替換桶頭
  • 實現弱一致性:迭代器反映操作啟動時的狀態,可容忍並發修改

擴容機制:多執行緒協作遷移

  • 觸發條件:size >= threshold 或 put 時發現桶為 ForwardingNode
  • 建立新表 nextTable(容量翻倍)
  • 每個執行緒領取遷移任務(transferIndex 原子遞減)
  • 遷移完成 CAS 替換 table 引用
  • Get/Put 遇到 ForwardingNode 自動轉向新表或協助遷移

關鍵複合操作(原子性保證)

方法語義替代同步容器模式
putIfAbsent(k, v)Key 不存在才放入if (!map.containsKey(k)) map.put(k, v)
remove(k, v)Key 且 Value 匹配才移除if (map.get(k).equals(v)) map.remove(k)
replace(k, oldV, newV)Key 且舊值匹配才替換if (map.get(k).equals(oldV)) map.put(k, newV)
computeIfAbsent(k, fn)Key 不存在則計算並放入if (!map.containsKey(k)) map.put(k, fn.apply(k))
merge(k, v, fn)Key 存在合併,不存在放入複雜條件更新

最佳實踐與選型指南

場景推薦容器理由
高併發 Map,讀多寫少ConcurrentHashMap無鎖讀、細粒度寫鎖、內建複合操作
高併發 List,讀多寫少CopyOnWriteArrayList寫時複製,讀無鎖,適合事件監聽器列表
高併發 QueueConcurrentLinkedQueue無鎖 CAS,非阻塞,高吞吐
阻塞隊列、生產消費ArrayBlockingQueue / LinkedBlockingQueue內建阻塞、流控、毒丸關閉
有序 Map、範圍查詢ConcurrentSkipListMap跳表實現,併發有序操作
集合視圖、SetConcurrentHashMap.newKeySet()基於 CHM,支援並發 Set 操作

避坑指南

  1. 不可用 putIfAbsent 替代 get-then-put 邏輯:語義不同,前者僅 Key 不存在
  2. size() 非精確值:併發環境下僅供參考,不可用於精確業務判斷
  3. 複合操作不可嵌套:map.computeIfAbsent(k, v -> map.get(k2)) 可能死鎖
  4. Null 不支援:Key/Value 不可為 null,用特殊標記物件替代
  5. 迭代器弱一致性:不拋 ConcurrentModificationException,但可能讀到舊值
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved