HashMap與ConcurrentHashMap詳解:實現原理、源碼分析與最佳實踐

引言

在Java編程中,集合框架是最常用的工具之一,而HashMap和ConcurrentHashMap則是其中使用頻率最高的兩個Map實現。它們都用于存儲鍵值對數據,但在實現機制、性能特點和適用場景上有著顯著差異。

HashMap作為單線程環境下的首選Map實現,以其O(1)的訪問效率和簡潔API贏得了廣泛應用;而ConcurrentHashMap則專為高并發環境設計,在保證線程安全的同時,提供了遠優于傳統同步集合的性能。

一、HashMap詳解

1. 基本概念與特性

HashMap是Java集合框架中的一個核心類,實現了Map接口,基于哈希表的原理,提供了高效的插入和查詢操作。它的主要特性包括:

  • 允許使用null作為鍵和值
  • 非線程安全
  • 不保證元素的順序
  • 基本操作(get和put)的時間復雜度接近于常數時間

HashMap的底層實現是基于數組+鏈表+紅黑樹(JDK 1.8之后)的復合結構,這種設計既保證了查詢效率,又解決了哈希沖突的問題。

2. 核心實現原理

2.1 存儲結構演進

HashMap的存儲結構隨著JDK版本的更新而演進:

??在JDK 1.7及之前版本中,HashMap采用數組+鏈表的結構。每個數組元素稱為一個桶(bucket),當發生哈希沖突時,同一個桶中的元素以鏈表形式存儲。

??JDK 1.8引入了重大改進,當鏈表長度超過閾值(默認為8)時,鏈表會轉換為紅黑樹,這大大提高了在哈希沖突嚴重情況下的查詢效率。當紅黑樹節點數量小于6時,又會退化回鏈表,這是為了平衡空間和時間成本。

2.2 hash方法原理

HashMap中的hash方法是確定元素存儲位置的關鍵,它的實現非常精妙:

static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

這個方法將key的hashCode值的高16位與低16位進行異或運算,目的是為了增加低位的隨機性,減少哈希沖突。因為在確定桶位置時,只會用到哈希值的低位(與數組長度-1進行與運算),所以這種設計能夠讓高位的特征也參與到計算中來。

2.3 確定桶位置

HashMap通過(n - 1) & hash計算鍵值對在數組中的位置,其中n是數組的長度,hash是通過hash方法計算得到的哈希值。

這種方式等同于取模運算hash % n,但位運算的效率更高。為了使這種方式有效,HashMap的容量始終是2的冪次方,這樣n-1的二進制表示全為1,進行與運算時能夠充分利用哈希值的每一位。

3. 擴容機制

HashMap的擴容是一個重要且復雜的過程,直接影響到其性能表現。

3.1 擴容觸發條件

??當HashMap中的元素數量超過負載因子與容量的乘積時,HashMap會進行擴容。默認情況下,初始容量是16,負載因子是0.75,這意味著當元素數量超過12時,會觸發擴容。

3.2 擴容過程

擴容過程包括以下步驟:

  1. 創建一個新的數組,容量是原來的兩倍
  2. 重新計算每個元素在新數組中的位置
  3. 將原數組中的元素轉移到新數組中

在JDK 1.7中,擴容過程中的元素遷移是頭插法,這在多線程環境下可能導致環形鏈表,從而引起死循環。

JDK 1.8對擴容過程進行了優化,采用尾插法,并且巧妙地利用了哈希值與舊容量的按位與運算,將元素分散到新桶的過程簡化為:元素要么在原位置,要么在原位置+舊容量的位置。

3.3 負載因子的選擇

負載因子是HashMap中一個重要的參數,它影響著空間利用率和查詢效率之間的平衡:

  • 負載因子越大,空間利用率越高,但沖突的概率也越高,查詢效率降低
  • 負載因子越小,沖突的概率越低,查詢效率高,但空間利用率降低

默認的0.75是一個經過大量實驗得出的比較合理的值,在時間和空間成本上取得了很好的平衡。在實際應用中,如果預知HashMap中元素的數量,可以在創建時指定初始容量,避免頻繁擴容帶來的性能損失。

4. 主要操作的實現

4.1 put操作

put操作是HashMap中最常用的方法之一,其實現邏輯如下:

  1. 如果HashMap未被初始化,則初始化
  2. 計算key的hash值
  3. 根據hash值確定在數組中的索引位置
  4. 如果該位置沒有元素,直接插入
  5. 如果該位置有元素,遍歷鏈表或紅黑樹
    • 如果找到相同的key,更新value
    • 如果沒有找到相同的key,插入新節點
  6. 檢查是否需要轉換為紅黑樹(JDK 1.8)
  7. 檢查是否需要擴容
4.2 get操作

get操作的實現相對簡單:

  1. 計算key的hash值
  2. 根據hash值確定在數組中的索引位置
  3. 如果該位置沒有元素,返回null
  4. 如果該位置有元素,遍歷鏈表或紅黑樹,查找相同的key
    • 如果找到,返回對應的value
    • 如果沒有找到,返回null
4.3 remove操作

remove操作的實現邏輯:

  1. 計算key的hash值
  2. 根據hash值確定在數組中的索引位置
  3. 如果該位置沒有元素,返回null
  4. 如果該位置有元素,遍歷鏈表或紅黑樹,查找相同的key
    • 如果找到,刪除節點并返回對應的value
    • 如果沒有找到,返回null

5. 線程不安全性分析

HashMap是非線程安全的,在多線程環境下可能出現各種問題:

  1. 在JDK 1.7中,并發擴容可能導致環形鏈表,從而引起死循環
  2. 并發put操作可能導致元素丟失
  3. 并發put和get操作可能導致get到臟數據

這些問題的根本原因是HashMap沒有任何同步機制,多個線程同時修改HashMap的內部結構時會相互干擾。在多線程環境下,應該使用ConcurrentHashMap或者Collections.synchronizedMap()來代替HashMap。

二、ConcurrentHashMap詳解

1. 基本概念與特性

ConcurrentHashMap是Java并發包中的重要成員,專為并發環境設計,提供了線程安全的Map實現。它的主要特性包括:

  • 線程安全,支持高并發訪問
  • 不允許使用null作為鍵或值
  • 檢索操作不需要鎖定
  • 迭代器是弱一致性的,不會拋出ConcurrentModificationException
  • 提供了比Hashtable更好的并發性能

ConcurrentHashMap通過巧妙的設計,在保證線程安全的同時,最大程度地減少了鎖競爭,提高了并發訪問的效率。

2. 實現原理演進

ConcurrentHashMap的實現原理在JDK 1.7和JDK 1.8中有顯著差異,體現了Java并發編程理念的演進。

2.1 JDK 1.7的分段鎖機制

在JDK 1.7中,ConcurrentHashMap采用了分段鎖(Segment)的設計:

  • 底層結構是Segment數組 + HashEntry數組 + 鏈表
  • 每個Segment相當于一個小型的HashMap
  • Segment繼承自ReentrantLock,提供了鎖的功能
  • 默認有16個Segment,支持16個線程并發寫入
  • Segment的個數一旦初始化就不能改變

這種設計的核心思想是將數據分成多個段,每個段獨立加鎖,這樣多個線程可以同時訪問不同的段,提高了并發性能。

2.2 JDK 1.8的設計革新

JDK 1.8中,ConcurrentHashMap進行了重大改進,摒棄了分段鎖的設計,采用了更加細粒度的鎖定機制:

  • 底層結構與HashMap類似,是Node數組 + 鏈表 + 紅黑樹
  • 鎖粒度更細,鎖定單個桶(數組中的每個位置)
  • 使用CAS(Compare and Swap)操作和synchronized關鍵字保證線程安全
  • 當鏈表長度超過8時,轉換為紅黑樹,提高查詢效率

這種設計進一步減少了鎖的粒度,提高了并發性能,同時簡化了代碼結構,使ConcurrentHashMap的實現更加優雅。

3. 并發控制機制

3.1 JDK 1.7的并發控制

在JDK 1.7中,ConcurrentHashMap的并發控制主要通過分段鎖實現:

  • 對于寫操作(put、remove等),需要先獲取對應Segment的鎖
  • 對于讀操作(get等),不需要加鎖,利用volatile關鍵字保證可見性
  • 不同Segment之間的寫操作可以并行執行,提高了并發性能
3.2 JDK 1.8的并發控制

JDK 1.8中,ConcurrentHashMap的并發控制更加精細:

  • 初始化數組時使用CAS操作保證線程安全
  • 更新操作(put、remove等)使用synchronized鎖定對應的桶
  • 讀操作不需要加鎖,利用volatile關鍵字保證可見性
  • 使用CAS操作進行計數和檢查是否需要擴容
  • 支持多線程并發擴容,提高擴容效率

這種設計充分利用了JDK 1.8中synchronized的優化,以及CAS操作的無鎖特性,在保證線程安全的同時,最大程度地提高了并發性能。

4. 主要操作的實現

4.1 put操作(JDK 1.8)

put操作是ConcurrentHashMap中最復雜的操作之一,其實現邏輯如下:

  1. 如果數組未初始化,先初始化數組
  2. 計算key的哈希值,確定在數組中的索引位置
  3. 如果該位置為空,使用CAS操作插入新節點
  4. 如果該位置的節點的哈希值為-1,說明正在擴容,則幫助擴容
  5. 否則,使用synchronized鎖定該節點,進行后續操作:
    • 如果是鏈表,遍歷鏈表查找相同的key
      • 如果找到,更新value
      • 如果沒找到,插入新節點到鏈表尾部
    • 如果是紅黑樹,按照紅黑樹的方式插入節點
  6. 檢查是否需要將鏈表轉換為紅黑樹
  7. 增加計數,檢查是否需要擴容
4.2 get操作(JDK 1.8)

get操作相對簡單,不需要加鎖:

  1. 計算key的哈希值,確定在數組中的索引位置
  2. 如果該位置為空,返回null
  3. 如果該位置的節點的key與查找的key相同,返回該節點的value
  4. 如果該位置是鏈表或紅黑樹,按照對應的數據結構查找節點
    • 如果找到,返回對應的value
    • 如果沒找到,返回null
4.3 size操作

在JDK 1.8中,ConcurrentHashMap使用一個volatile變量baseCount和一個CounterCell數組來記錄元素個數:

  • 在沒有競爭的情況下,直接更新baseCount
  • 在有競爭的情況下,使用CounterCell數組分散計數
  • 獲取size時,將baseCount和所有CounterCell的值相加

這種設計避免了全局鎖定,提高了并發性能。

5. 擴容機制

5.1 JDK 1.7擴容

在JDK 1.7中,每個Segment獨立擴容,擴容過程與HashMap類似:

  1. 創建一個新的HashEntry數組,容量是原來的兩倍
  2. 遍歷原數組中的每個元素,重新計算哈希值,放入新數組
  3. 將新數組賦值給Segment
5.2 JDK 1.8擴容

在JDK 1.8中,擴容過程更加復雜,但支持多線程并發擴容:

  1. 創建一個新的Node數組,容量是原來的兩倍
  2. 將數組分成多個區段(stride),每個線程負責一個區段的遷移
  3. 使用ForwardingNode標記已經遷移完成的桶
  4. 當所有桶都遷移完成后,將新數組賦值給table屬性

這種設計允許多個線程同時參與擴容過程,大大提高了擴容效率。

三、HashMap與ConcurrentHashMap對比分析

1. 線程安全性對比

  • HashMap:非線程安全,在多線程環境下可能導致死循環、數據丟失等問題
  • ConcurrentHashMap:線程安全,專為并發環境設計,通過精細的鎖機制和CAS操作保證線程安全

2. 性能對比

  • HashMap:單線程環境下性能較好,沒有同步開銷
  • ConcurrentHashMap:在高并發環境下性能較好,但在單線程環境下由于同步開銷,性能略低于HashMap

具體性能差異取決于并發程度、數據規模和操作類型。在實際應用中,應根據具體場景選擇合適的實現。

3. 實現機制對比

特性HashMapConcurrentHashMap (JDK 1.8)
底層數據結構數組 + 鏈表 + 紅黑樹數組 + 鏈表 + 紅黑樹
線程安全機制synchronized + CAS
允許null鍵值
擴容機制單線程擴容多線程并發擴容
哈希沖突解決鏈表 + 紅黑樹鏈表 + 紅黑樹

3. 適用場景分析

  • HashMap適用于:

    • 單線程環境
    • 讀多寫少的場景
    • 對性能要求高的場景
    • 需要使用null鍵或值的場景
  • ConcurrentHashMap適用于:

    • 多線程并發環境
    • 讀寫都比較頻繁的場景
    • 對線程安全有要求的場景
    • 需要較高并發性能的場景

四、最佳實踐與性能優化

1. 合理設置初始容量

無論是HashMap還是ConcurrentHashMap,合理設置初始容量都能有效減少擴容次數,提高性能。如果能預估元素數量,應該在創建時指定合適的初始容量。

計算公式:initialCapacity = expectedSize / loadFactor + 1

2. 選擇合適的負載因子

負載因子影響著空間利用率和查詢效率,默認值0.75在大多數情況下是合適的。但在特定場景下,可以根據需求調整:

  • 如果內存充足,對時間效率要求高,可以降低負載因子
  • 如果內存緊張,對空間利用率要求高,可以提高負載因子

3. 避免頻繁擴容

頻繁擴容會導致性能下降,特別是對于大容量的Map。可以通過以下方式避免:

  • 預估元素數量,合理設置初始容量
  • 批量添加元素時,先計算最終容量,一次性擴容
  • 對于固定大小的數據集,可以禁用自動擴容

4. 合理使用多線程

在使用ConcurrentHashMap時,應該充分利用其并發特性:

  • 避免不必要的同步操作
  • 利用ConcurrentHashMap提供的原子操作方法
  • 合理設置并發級別,避免過多線程競爭

5. 常見陷阱與注意事項

  • 避免在多線程環境下使用HashMap
  • 注意ConcurrentHashMap不支持null鍵和值
  • 理解ConcurrentHashMap的弱一致性特性
  • 避免在迭代過程中修改Map結構
  • 注意自定義對象作為鍵時,必須正確實現hashCode()和equals()方法

總結

本文深入分析了HashMap和ConcurrentHashMap的實現原理、性能特點和適用場景。通過對比這兩種重要的Map實現,我們可以看到Java集合框架在單線程和多線程環境下的不同設計思路。

HashMap憑借其簡單高效的特性,在單線程環境中表現出色;而ConcurrentHashMap則通過精心設計的并發控制機制,在保證線程安全的同時,提供了優異的并發性能。在實際開發中,應根據具體場景選擇合適的實現,并遵循最佳實踐,以獲得最佳性能。

理解這兩種數據結構的內部工作原理,不僅有助于我們更好地使用它們,也能幫助我們在設計自己的數據結構時借鑒其中的優秀思想。

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

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

相關文章

CSS之動畫(奔跑的熊、兩面反轉盒子、3D導航欄、旋轉木馬)

一、 2D轉換 1.1 transform: translate( ) 轉換(transform) 是CSS3中具有顛覆性的特征之一,可以實現元素的位移、旋轉、縮放等效果 移動:translate 旋轉:rotate 縮放:scale 下圖為2D轉換的坐標系 回憶…

【筆記】在 MSYS2(MINGW64)中安裝 python-maturin 的記錄

#工作記錄 📌 安裝背景 操作系統:MSYS2 MINGW64當前時間:2025年6月1日Python 版本:3.12(通過 pacman 安裝)目標工具:maturin —— 用于構建和發布 Rust 編寫的 Python 包 🛠? 安裝…

基于微信小程序的垃圾分類系統

博主介紹:java高級開發,從事互聯網行業六年,熟悉各種主流語言,精通java、python、php、爬蟲、web開發,已經做了六年的畢業設計程序開發,開發過上千套畢業設計程序,沒有什么華麗的語言&#xff0…

工作日記之權限校驗-token的實戰案例

背景說明 我們組負責維護的一個系統,前端界面掛載在其他兩個系統上,因為歷史遺留原因,同時也掛在公網上,沒有登陸功能和用戶體系,只要輸入網址就能訪問,雖然這個系統是給公司內部人員使用,但是…

mysql雙主模式下基于keepalived的虛擬ip實現高可用模式搭建

數據庫安裝和升級和雙主配置的操作可以參考我的另一篇文章: 數據庫安裝和升級和雙主配置 1、在兩臺服務器都下載和安裝keepalived 下載: yumdownloader --resolve keepalived 下載后得到: [rootlocalhost keepalivedRpm]# ll 總用量 1896 …

展會聚焦丨漫途科技亮相2025西北水務博覽會!

2025第三屆西北水務數字化發展論壇暨供排水節水灌溉新技術設備博覽會在蘭州甘肅國際會展中心圓滿落幕。本屆展會以“科技賦能水資源,數智引領新動能”為主題,活動匯集水務集團、科研院所、技術供應商等全產業鏈參與者,旨在通過前沿技術展示與…

單調棧(打卡)

本篇基于b站靈茶山艾府。 下面是靈神上課講解的題目與課后作業,課后作業還有三道實在寫不下去了,下次再寫。 739. 每日溫度 給定一個整數數組 temperatures ,表示每天的溫度,返回一個數組 answer ,其中 answer[i] 是…

【機器學習基礎】機器學習入門核心算法:層次聚類算法(AGNES算法和 DIANA算法)

機器學習入門核心算法:層次聚類算法(AGNES算法和 DIANA算法) 一、算法邏輯二、算法原理與數學推導1. 距離度量2. 簇間距離計算(連接標準)3. 算法偽代碼(凝聚式) 三、模型評估1. 內部評估指標2. …

已有的前端項目打包到tauri運行(windows)

1.打包前端項目產生靜態html、css、js 我們接下來用vue3 vite編寫一個番茄鐘案例來演示。 我們執行npm run build 命令產生的dist目錄下的靜態文件。 2.創建tarui項目 npm create tauri-applatest一路回車,直到出現。 3.啟動運行 我們將打包產生的dist目錄下的…

Unity3D仿星露谷物語開發55之保存地面屬性到文件

1、目標 將游戲保存到文件,并從文件中加載游戲。 Player在游戲中種植的Crop,我們希望保存到文件中,當游戲重新加載時Crop的GridProperty數據仍然存在。這次主要實現保存地面屬性(GridProperties)信息。 我們要做的是…

Java面試:企業協同SaaS中的技術挑戰與解決方案

Java面試:企業協同SaaS中的技術挑戰與解決方案 面試場景 在一家知名互聯網大廠,面試官老王正在對一位應聘企業協同SaaS開發職位的程序員謝飛機進行技術面試。 第一輪提問:基礎技術 老王:謝飛機,你好。首先&#xf…

SQL注入速查表(含不同數據庫攻擊方式與差異對比)

1. 字符串連接 字符串連接是SQL注入中常用的操作,用于將多個字符串拼接為一個,以構造復雜的注入語句。不同數據庫的字符串連接語法存在顯著差異,了解這些差異有助于精準構造payload。 Oracle:使用||操作符進行字符串連接&#xf…

uni-data-picker級聯選擇器、fastadmin后端api

記錄一個部門及部門人員選擇的功能,效果如下: 組件用到了uni-ui的級聯選擇uni-data-picker 開發文檔:uni-app官網 組件要求的數據格式如下: 后端使用的是fastadmin,需要用到fastadmin自帶的tree類生成部門樹 &#x…

Mac電腦上本地安裝 redis并配置開啟自啟完整流程

文章目錄 一、安裝 Redis方法 1:通過源碼編譯安裝(推薦)方法 2:通過 Homebrew 安裝(可選) 二、配置 Redis1. 創建配置文件和數據目錄2. 修改配置文件 三、配置開機自啟1、通過 launchd 系統服務&#xff08…

wsl安裝linux

安裝wsl 啟用適用于 Linux 的 Windows 子系統 以管理員身份打開 PowerShell (> PowerShell > 右鍵單擊 > 以管理員身份運行) 并輸入以下命令,然后重啟 dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsyste…

OpenGL 3D 編程

OpenGL 是一個強大的跨平臺圖形 API,用于渲染 2D 和 3D 圖形。以下是 OpenGL 3D 編程的入門基礎。 一. 環境設置 安裝必要的庫 GLFW: 用于創建窗口和處理輸入 GLEW 或 GLAD: 用于加載 OpenGL 函數 GLM: 數學庫,用于 3D 變換 // 基本 OpenGL 程序結構示例 #include <GL/g…

Android基于LiquidFun引擎實現軟體碰撞效果

一、實現效果 Android使用LiquidFun物理引擎實現果凍碰撞效果 二、Android代碼 // 加載liquidfun動態庫static {System.loadLibrary("liquidfun");System.loadLibrary("liquidfun_jni");}class ParticleData {long id;ParticleSystem particleSystem;float…

Redis持久化機制詳解:RDB與AOF的深度剖析

一、為什么需要持久化&#xff1f; Redis作為內存數據庫&#xff0c;數據存儲在易失性內存中。持久化機制解決兩大核心問題&#xff1a; 數據安全&#xff1a;防止服務器宕機導致數據丟失災難恢復&#xff1a;支持數據備份與快速重建 二、RDB&#xff1a;內存快照持久化 ? …

Netty學習example示例

文章目錄 simpleServer端NettyServerNettyServerHandler Client端NettyClientNettyClientHandler tcp&#xff08;粘包和拆包&#xff09;Server端NettyTcpServerNettyTcpServerHandler Client端NettyTcpClientNettyTcpClientHandler protocolcodecCustomMessageDecoderCustomM…

ThreadLocal ,底層原理,強引用,弱引用,內存泄漏

目錄 ThreadLocal的基本概念 底層實現原理 強引用與弱引用 內存泄漏問題 內存泄漏的解決方案 示例代碼 ThreadLocal的基本概念 ThreadLocal是Java中的一個類&#xff0c;位于java.lang包下&#xff0c;它提供了線程局部變量的功能。每個使用該變量的線程都有自己獨立的初…