【多線程篇22】:ConcurrentHashMap的并發安全原理剖析

文章目錄

  • 一、HashMap 的“不安全”:問題的根源
    • 1. 數據結構回顧 (JDK 1.8)
    • 2. 并發下的致命缺陷:`put` 操作
  • 二、ConcurrentHashMap 的安全之道 (JDK 1.8+)
    • 1. 核心數據結構
    • 2. 安全的 `put` 操作:分場景精細化加鎖
    • 3. 安全的 `size()` 計算:并發計數
  • 三、表格總結

在 Java 面試和日常開發中, HashMapConcurrentHashMap (以下簡稱 CHM) 是我們繞不開的兩個核心類。我們都知道它們的關鍵區別是“線程安全”: HashMap 是非線程安全的,而 CHM 是線程安全的。

那么,這種“安全”是如何實現的?CHM 到底比 HashMap 高明在哪里?為什么 JDK 1.8 的 CHM 性能遠超早期版本?

本文將帶你深入 JDK 1.8+ 的源碼,徹底搞懂 CHM 并發安全的底層奧秘。

一、HashMap 的“不安全”:問題的根源

在分析 CHM 的精妙設計之前,我們必須先理解 HashMap 在并發環境下為什么會“翻車”。

1. 數據結構回顧 (JDK 1.8)

HashMap 的底層是 “數組 + 鏈表 / 紅黑樹”

// HashMap.java
transient Node<K,V>[] table;

table 是一個 Node 數組。當多個元素的 hash 值沖突時,它們會以鏈表的形式存放在同一個數組索引(桶)下;當鏈表長度超過 TREEIFY_THRESHOLD (默認為8) 且數組長度大于 64 時,鏈表會轉化為紅黑樹以提高查詢效率。

2. 并發下的致命缺陷:put 操作

HashMap 的所有操作都沒有任何同步措施。我們以 putVal 方法為例,看看在并發下會發生什么。

// HashMap.java -> putVal() 核心邏輯簡化
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;// 1. 檢查 table 是否為空,如果為空則初始化 (resize)if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 2. 計算索引 i,檢查該位置是否為空if ((p = tab[i = (n - 1) & hash]) == null)// 3. 如果為空,直接創建一個新節點放在這里tab[i] = newNode(hash, key, value, null);else {// ... 省略桶內已有元素時的處理邏輯}// ...return null;
}

經典的“數據覆蓋”競爭條件 (Race Condition):

假設兩個線程 T1 和 T2 同時向一個空桶 tab[i]put 數據。

  1. T1 執行到第 2 步,p = tab[i],發現 pnull
  2. 此時發生線程切換,T2 開始執行。
  3. T2 也執行到第 2 步,p = tab[i],發現 p 同樣是 null
  4. T2 繼續執行第 3 步,成功將自己的新節點放入 tab[i]
  5. 線程切換回 T1。由于 T1 之前已經判斷過 pnull,它不會再檢查一次,而是直接執行第 3 步,將自己的新節點放入 tab[i]
  6. 結果:T1 的數據覆蓋了 T2 的數據,導致 T2 的寫入操作丟失

除了數據覆蓋,并發下的 resize() 操作在 JDK 1.7 中甚至會因頭插法導致鏈表形成環形結構,造成 get 操作時死循環。雖然 JDK 1.8 改為尾插法修復了此問題,但數據丟失等并發問題依然存在。

結論HashMap 的不安全,源于其所有操作都是“非原子性”的,且缺乏內存可見性保障。


二、ConcurrentHashMap 的安全之道 (JDK 1.8+)

JDK 1.8 對 CHM 進行了革命性的重構,摒棄了 JDK 1.7 的分段鎖(Segment)機制,采用了更為精細的 CAS + synchronized + volatile 方案,實現了更高的并發性能。

1. 核心數據結構

// ConcurrentHashMap.java
transient volatile Node<K,V>[] table;// Node 定義 (部分)
static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;volatile V val;       // value 是 volatile 的volatile Node<K,V> next; // next 指針是 volatile 的
}

關鍵點

  • table 數組被 volatile 修飾:保證其可見性,當一個線程對 table 進行擴容或修改后,其他線程能立刻看到。
  • Nodevalnext 指針被 volatile 修飾:保證了節點值或鏈表結構的修改對其他線程的可見性。這是無鎖讀(get 操作)的關鍵基礎。

2. 安全的 put 操作:分場景精細化加鎖

CHM 的 put 方法是其并發設計的精髓所在。它通過 CASsynchronized 的配合,將鎖的粒度降到了最低。

我們來分析其核心方法 putVal 的源碼:

// ConcurrentHashMap.java -> putVal() 核心邏輯簡化
final V putVal(K key, V value, boolean onlyIfAbsent) {// ... key/value 不允許為 nullint hash = spread(key.hashCode());int binCount = 0;// (1) 無限循環,直到成功插入或找到已有keyfor (Node<K,V>[] tab = table;;) {Node<K,V> f; int n, i, fh;// (2) 場景一:table 未初始化,CAS 初始化if (tab == null || (n = tab.length) == 0)tab = initTable(); // CAS操作,只有一個線程能成功// (3) 場景二:目標桶為空,CAS 插入節點 (無鎖)else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))break; // CAS 成功,直接跳出循環}// (4) 場景三:檢測到正在擴容else if ((fh = f.hash) == MOVED)tab = helpTransfer(tab, f); // 幫助其他線程一起擴容// (5) 場景四:目標桶有值,鎖住頭節點else {V oldVal = null;synchronized (f) { // f 是桶的頭節點if (tabAt(tab, i) == f) { // 再次確認頭節點沒變// 在同步塊內,遍歷鏈表或紅黑樹if (fh >= 0) {// ... 鏈表遍歷邏輯 ...} else if (f instanceof TreeBin) {// ... 紅黑樹處理邏輯 ...}}}// ...break;}}addCount(1L, binCount); // 更新 sizereturn null;
}

剖析其安全機制:

  • 無鎖讀(get 操作):得益于 tableNode.valNode.next 都是 volatile 的,get 操作不需要加任何鎖。volatile 保證了讀線程總能看到其他寫線程對數據的最新修改,從而實現了高效的無鎖讀取。

  • put 操作的安全性分析

    1. 初始化安全 (場景二)initTable() 方法內部使用 CAS 操作來設置 sizeCtl 狀態位,保證了在多線程同時初始化時,只有一個線程能成功執行,其他線程則會 yield 等待。
    2. 空桶插入安全 (場景三):當目標桶為空時,CHM 并未直接加鎖,而是樂觀地嘗試使用 CAS (casTabAt) 操作將新節點放入。
      • casTabAt 是一個原子操作,它會比較 tab[i] 的內存值是否為 null,如果是,就將其更新為新節點。
      • 如果 CAS 成功,說明沒有競爭,操作完成。
      • 如果失敗,說明在它操作的瞬間,有另一個線程捷足先登了。此時 for 循環會繼續,重新讀取 tab[i] 的新值,進入下一個場景(通常是場景五)。
    3. 非空桶插入安全 (場景五):這是最關鍵的部分。當桶中已有節點時,不能再用 CAS 了(因為需要遍歷鏈表)。此時,CHM 采用 synchronized 關鍵字,但它鎖住的不是整個 table,而是這個桶的頭節點 f
      • 鎖粒度極小:只鎖住當前要操作的桶,其他桶的操作完全不受影響,并發度極高。
      • 為什么不鎖 StringIntegersynchronized 鎖的是對象。如果鎖 key,當 key 是常量池中的字符串時,可能會鎖住一個全局共享的對象,造成意想不到的死鎖。鎖住桶的頭節點 Node 對象是最安全和高效的選擇。
    4. 擴容安全 (場景四):當一個線程在 put 時發現桶的頭節點 hash 值為 MOVED,表示 table 正在擴容。此時它不會等待,而是調用 helpTransfer 加入到擴容大軍中,幫助移動數據,充分利用了 CPU 資源,實現并發擴容。

3. 安全的 size() 計算:并發計數

HashMapsize 只是一個簡單的成員變量 int size。多線程下 ++size 是非原子操作,會導致計數不準。

ConcurrentHashMapsize() 實現非常巧妙,它避免了全局鎖,以分散計數的方式實現。

  • baseCount:一個基礎計數值,在沒有并發競爭時,優先通過 CAS 更新這個值。
  • CounterCell[]:一個計數器單元數組。當 CAS 更新 baseCount 失敗(說明存在競爭)時,線程會找到 CounterCell 數組中的一個槽位,對這個槽位里的計數值進行更新。
  • size() 計算:最終的大小是 baseCount 加上所有 CounterCell 數組中計數的總和。

這種條帶化計數(Striped Counter) 的思想,將對單一 size 變量的競爭壓力分散到多個 CounterCell 上,極大地提高了高并發下的計數性能。


三、表格總結

特性HashMap (JDK 1.8+)ConcurrentHashMap (JDK 1.8+)
線程安全
核心數據結構Node[] tablevolatile Node<K,V>[] table
加鎖機制無鎖CAS + synchronized(鎖桶頭節點)
put 操作非原子性,并發下有數據覆蓋風險1. 空桶:CAS 無鎖插入
2. 非空桶:synchronized 鎖住桶頭節點
get 操作非線程安全無鎖,通過 volatile 保證可見性
size() 計算int size 成員變量,非線程安全baseCount + CounterCell[],并發安全高效
擴容機制單線程擴容,并發下危險并發擴容,多線程可協同完成
Key/Value可為 null均不可為 null (避免二義性)
性能單線程下極高高并發下性能優異,單線程下因 volatileCAS 略遜于HashMap

結論:

  • volatile 作為基石,保證了多線程間的內存可見性,是無鎖讀和后續操作的基礎。
  • CAS 作為先鋒,處理無競爭或低競爭場景(如初始化、空桶插入),避免了不必要的加鎖開銷。
  • synchronized 鎖住桶頭節點作為主力,在必須同步的場景下,將鎖的粒度降到最低,實現了極高的并發度。
  • 用并發擴容和并發計數等輔助策略,將并發思想貫徹到每一個細節,榨干硬件性能。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/90048.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/90048.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/90048.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【Java + Vue 實現圖片上傳后 導出圖片及Excel 并壓縮為zip壓縮包】

系統環境&#xff1a; Java JDK&#xff1a;1.8.0_202 Node.js&#xff1a;v12.2.0 Npm&#xff1a;6.9.0 Java后端實現 Controller /*** xxxx-導出* param response 返回信息體* param files 上傳的圖片文件* param param1 參數1* param param2 參數2*/PostMapping("/ex…

安科瑞:能源微電網助力工業園區“綠色”發展

朱以真近日&#xff0c;廈門市工業和信息化局印發工業園區綠色智慧微電網建設&#xff0c;擬開展全市工業園區綠色智慧微電網試點通知&#xff0c;那么對于如何實現綠色園區的建設是今天的話題。對工業園區綠色智慧微電網建設需求&#xff0c;其核心價值體現在“源-網-荷-儲-充…

VUE2 學習筆記3 v-on、事件修飾符、鍵盤事件

事件處理v-on用于事件交互。語法&#xff1a;v-on:要綁定的事件“事件觸發時執行的函數” &#xff08;函數這里可以寫括號&#xff0c;也可以不寫&#xff0c;沒有影響&#xff09;簡寫&#xff1a;:事件觸發時要執行的函數&#xff0c;在Vue配置參數中&#xff0c;通過method…

變換域通訊系統CCSK的matlab仿真

CCSK&#xff08;Cyclic Code Shift Keying&#xff09;通信系統的MATLAB仿真。實現完整的CCSK調制、AWGN信道傳輸和解調過程&#xff0c;并計算了誤碼率&#xff08;BER&#xff09;。 % CCSK通信系統仿真 clear; clc; close all;% 參數設置 L 31; % m序列…

技術演進中的開發沉思-40 MFC系列:多線程協作

今天說說MFC的線程&#xff0c;當年用它實現中間件消息得心應手之時&#xff0c;可以實現一邊實時接收數據&#xff0c;一邊更新界面圖表圖文信息&#xff0c;順滑得讓人想吹聲口哨。 MFC 多線程它像給程序裝上了分身術&#xff0c;讓原本只能 “單任務跑腿” 的代碼&#xff0…

高速公路自動化安全監測主要內容

近年來&#xff0c;隨著社會經濟的快速發展&#xff0c;高速公路的通車里程不斷增加&#xff0c;交通流量日益增大。與此同時&#xff0c;高速公路交通事故數量也呈現出一定的增長趨勢。這些事故不僅造成了大量的人員傷亡和財產損失&#xff0c;還嚴重影響了社會的穩定和經濟的…

完美解決 Ubuntu 中自定義啟動器圖標重復的問題(以 MATLAB 為例)

如果你在 Ubuntu 上為 MATLAB、PyCharm、Android Studio 或其他第三方應用創建了自定義啟動器&#xff0c;并把它固定到了左側的 Dock 欄&#xff0c;你很可能遇到過這個令人抓狂的場景&#xff1a; 當你滿心歡喜地點擊固定的圖標啟動程序后&#xff0c;Dock 欄上竟然“憑空”冒…

一文讀懂深度模型優化器,掌握煉丹工具

深度模型優化器是訓練神經網絡的核心工具&#xff0c;其目標是高效地找到損失函數的最小值。從基礎的隨機梯度下降&#xff08;SGD&#xff09;到結合一階動量修正與兩階段更新的Ranger&#xff0c;優化器的發展始終圍繞著加速收斂、提升穩定性、適應參數差異和增強泛化能力四大…

關于虛擬機部署的k8s集群calico組件在開機恢復快照時的問題

在恢復虛擬機開機快照的時候總會出現各種的網絡問題&#xff0c;連接不到自己的443端口&#xff0c;集群狀態異常等等&#xff0c;繞一圈都發現不了問題&#xff0c;其實根本就沒問題。虛擬機環境再現&#xff0c;恢復快照后的k8s環境&#xff0c;nfs莫名宕掉提示此報錯&#x…

從感知到決策:虛擬仿真系統與視覺算法融合下的多路RTSP視頻接入技術探究

1?? 背景概述 隨著國防信息化和智能化進程的不斷加快&#xff0c;虛擬仿真系統 與 智能視覺算法平臺 正逐步成為現代化裝備研發、測試驗證與戰術訓練的重要技術支撐。相比傳統的靜態建模或離線推演&#xff0c;新一代作戰仿真與智能系統更強調 實時性、感知能力與動態交互&a…

Android app如果不適配Android 15會怎么樣

深度思考完成 用時49秒 首先&#xff0c;用戶的問題是&#xff1a;“Android app如果不適配Android 15會怎么樣&#xff1f;”我需要基于搜索結果來回答。 讓我回顧搜索結果&#xff1a; ?1&#xff1a;標題是“Android 15應用適配指南”&#xff0c;內容強調了不適配Androi…

Vue-21-利用Vue3的axios+Python的flask實現前后端交互功能

文章目錄 1 任務需求 2 前端 2.1 模板template 2.2 JS代碼script 2.3 Calculate.vue(子組件) 2.4 App.vue(根組件) 3 后端 3.1 導入模塊 3.2 創建應用實例 3.3 配置CORS 3.4 定義路由 3.5 處理請求 3.6 main.py 4 附錄 4.1 CORS 4.1.1 全局啟用CORS 4.1.2 限制允許的域名(更安…

動態規劃之最長回文子串

題目&#xff1a;最長回文子串 給你一個字符串 s&#xff0c;找到 s 中最長的 回文 子串。 示例 1&#xff1a; 輸入&#xff1a;s “babad” 輸出&#xff1a;“bab” 解釋&#xff1a;“aba” 同樣是符合題意的答案。 示例 2&#xff1a; 輸入&#xff1a;s “cbbd” 輸…

Linux 編程中的錯誤處理機制詳解 —— `errno` 全解析

文章目錄Linux 編程中的錯誤處理機制詳解 —— errno 全解析一、什么是 errno&#xff1f;?為什么需要 errno&#xff1f;? 它在哪里定義&#xff1f;二、errno 的設置與讀取規則?? errno 不是總是有效&#xff01;?使用 errno 的正確步驟&#xff1a;三、與 errno 配套使…

力扣-最長遞增子序列

簡單記錄學習~給你一個整數數組 nums &#xff0c;找到其中最長嚴格遞增子序列的長度。子序列 是由數組派生而來的序列&#xff0c;刪除&#xff08;或不刪除&#xff09;數組中的元素而不改變其余元素的順序。例如&#xff0c;[3,6,2,7] 是數組 [0,3,1,6,2,2,7] 的子序列。示例…

公司內部網址怎么在外網打開?如何讓外網訪問內網的網站呢?

很多公司內部本地會部署有中小型的服務器&#xff0c;可以很好的方便用于一些辦公業務系統&#xff0c;或測試開發需要。在數字化辦公和生活場景中&#xff0c;除了公司內部局域網內訪問公司系統外&#xff0c;經常會遇到需要讓外網訪問內網網站的情況。比如企業員工遠程辦公時…

有趣的css - 多選立體標簽按鈕

&#x1f36d; 大家好&#xff0c;我是 Just&#xff0c;這里是「設計師工作日常」&#xff0c;今天分享的是一個交互較完整的多選立體標簽按鈕。 最新文章通過公眾號「設計師工作日常」發布。 目錄整體效果核心代碼html 代碼css 部分代碼完整代碼如下html 頁面css 樣式頁面渲…

C++中byte*和char*的區別

在C中&#xff0c;byte*&#xff08;通常指 std::byte*&#xff09;和 char* 都是指針類型&#xff0c;但它們在語義和用途上有重要區別&#xff1a;1. 類型定義char* char 是C內置的基本類型&#xff0c;表示字符&#xff08;通常是1字節&#xff09;。 char* 常用于&#xff…

【node】npm包本地開發與調試

npm link 進入本地的 babel-plugin-function-try-catch 這個 npm 包的根目錄執行&#xff1a; npm link上面的命令可以將當前的這個包安裝在全局&#xff08;mac 中的路徑是 /usr/local/bin&#xff09;&#xff0c;也就是 npm i -g 安裝包的目錄。 執行后結果如圖&#xff…

突破量子仿真瓶頸:微算法科技MLGO量子算法的算術化與核操作迭代模型

近年來&#xff0c;量子計算機的迅速發展和潛在的強大計算能力吸引了全球科研機構和企業的廣泛關注。量子計算機利用量子力學的特性來處理復雜的計算任務&#xff0c;具有在某些方面遠超經典計算機的潛力。然而&#xff0c;真正實用的量子計算機尚未大規模普及&#xff0c;因此…