主頁 > 作業系統 > HashMap原始碼實作分析

HashMap原始碼實作分析

2020-09-13 20:02:55 作業系統

HashMap原始碼實作分析

一、前言

HashMap 顧名思義,就是用hash表的原理實作的Map介面容器物件,那什么又是hash表呢,

我們對陣列都很熟悉,陣列是一個占用連續記憶體的資料結構,學過C的朋友對這一點影響肯定更為深刻,既然是一段連續的記憶體,陣列的特點就顯而易見了,一旦你知道要查第幾個資料,時間復雜度就是O(1),但是對于插入操作就很困難;還有一種資料結構你也一定很熟悉,那就是鏈表,鏈表由一組指向(單向或者雙向)的節點連接的資料結構,它的特點是記憶體不連續,查找困難,但是插入洗掉都很容易,

那有沒有一種查找容易,插入洗掉查找都容易的資料結構呢, 沒錯,它就是hash表,

本篇,我們就來討論:

  • HashMap的資料結構實作方式
  • HashMap是怎么做到為get、put操作提供穩定的時間復雜度的
  • HashMap什么時候從單節點轉成鏈表又是什么時候從鏈表轉成紅黑樹
  • HashMap初始化時為什么要給自定義的初始容量,
  • HashMap如何保證容量始終是2的冪
  • HashMap為何要保證容量始終是2的冪
  • HashMap的hash值如何計算
  • HashMap為什么是執行緒不安全的

要了解HashMap 最好的方式就是看原始碼,本篇內容基于Jdk1.8HashMap原始碼,

二、HashMap的基本要素

磨刀不誤砍柴功,想了解HashMap的原理,必然繞不過HashMap原始碼中的以下幾個變數:

  • DEFAULT_INITIAL_CAPACITY: 初始容量 1<<4也就是16
  • MAXIMUM_CAPACITY:最大容量 1<<30,
  • DEFAULT_LOAD_FACTOR:負載因子,默認是0.75,什么意思呢,比如說你定義了一個初始容量為16的HashMap,當你不斷向里面添加元素后,最多到初始容量的0.75,HashMap就會觸發擴容操作,
  • threshold:下一個觸發擴容操作的閾值,threshold = CAPACITY * LOAD_FACTOR,
  • TREEIFY_THRESHOLD:鏈表轉紅黑樹閾值,默認為8,超過8就會執行鏈表轉紅黑樹方法,但是注意轉紅黑樹方法中會判斷當前size是否大于64,只有大于64才轉紅黑樹,否則執行resize()操作
  • UNTREEIFY_THRESHOLD: 紅黑樹轉鏈表閾值,默認為6,顧名思義,紅黑樹節點小于6就會轉成鏈表,
  • Node<K, V> implements Map.Entry<K, V> HashMap存放資料的基本單位,里面存有hash值、key、value、next,
  • Node<K, V>[] table:存放Node節點的陣列,HashMap最底層陣列,陣列元素可以為單節點Node、多節點鏈表、多節點紅黑樹,

以上內容,有個印象就好,不必每個都記得,但這些概念對理解HashMap至關重要,

三、正文

3.1 HashMap 資料結構

HashMap的資料結構很簡單,它是一個Node型別的陣列,每個元素可以為單節點、多節點鏈表、多節點紅黑樹,關鍵的問題是,這么簡單的結構怎么實作的put、get都很快? 什么時候從單節點轉成鏈表又是什么時候從鏈表轉成紅黑樹?

3.1.1 HashMap如何實作put、get操作時間復雜度為O(1)~O(n)?

我們知道,查找一個陣列的元素,當我們不知道index的時候,復雜度是很高的,但是當我們知道index的時候,這個復雜度就是O(1)級別的,HashMap使用的就是這個原理,
對于get操作,首先根據key計算出hash值,而這個hash值執行操作(n - 1) & hash后就是它所在的index,在最好的情況下,該index恰好只有一個節點且hash值和key的hash值相同,那么時間復雜度就是O(1),當該節點為鏈表或者紅黑樹時,時間復雜度會上升,但是由于HashMap的優化(鏈表長度、紅黑樹長度相對于HashMap容量不會過長,過長會觸發resize操作),所以最壞的情況也就是O(n),可能還會小于這個值,

對于put操作,我們知道,陣列插入元素的成本是高昂的,HashMap巧妙的使用鏈表和紅黑樹代替了陣列插入元素需要移動后續元素的消耗,這樣在最好的情況下,插入一個元素,該index位置恰好沒有元素的話,時間復雜度就是O(1),當該位置有元素且為鏈表或者紅黑樹的情況下,時間復雜度會上升,但是最壞的情況下也就是O(n),

3.1.2 HashMap什么時候從單節點轉成鏈表又是什么時候從鏈表轉成紅黑樹?

單節點轉鏈表很簡單,當根據新加入的值計算出來的index處有元素時,若元素為單節點,則從節點轉為鏈表,
鏈表轉紅黑樹有兩個條件:

  • 鏈表長度大于TREEIFY_THRESHOLD,默認閾值是8

  • HashMap長度大于64

當同時滿足這兩個條件,那么就會觸發鏈表轉紅黑樹的操作,

3.2 HashMap初始化時為什么要給自定義的初始容量?

為啥前輩們都要求定義一個HashMap的時候一定要使用建構式HashMap(int initialCapacity)指定初始容量呢?

在阿里的《Java開發手冊》中是這樣說明的:

  1. 【推薦】集合初始化時,指定集合初始值大小,

說明:HashMap 使用 HashMap(int initialCapacity) 初始化,

正例:initialCapacity = (需要存盤的元素個數 / 負載因子) + 1,注意負載因子(即 loader

factor)默認為 0.75,如果暫時無法確定初始值大小,請設定為 16(即默認值),

反例:HashMap 需要放置 1024 個元素,由于沒有設定容量初始大小,隨著元素不斷增加,容

量 7 次被迫擴大,resize 需要重建 hash 表,嚴重影響性能,

這個問題在HashMap原始碼中是顯而易見的,每次put函式中都會檢查當前size是否大于threshold,如果大于就會進行擴容,新容量是原來容量的二倍,那么問題就來了,當你要存大量資料到HashMap中而又不指定初始容量的話,擴容會被一次接一次的觸發,非常消耗性能,

而初始容量和負載因子給多少好呢,日常開發中如無必要不建議動負載因子,而是根據要使用的HashMap大小確定初始容量,這也不是說為了避免擴容初始容量給的越大越好, 越大申請的記憶體就越大,如果你沒有這么多資料去存,又會造成hash值過于離散,

3.3 HashMap如何保證容量始終是2的冪

HashMap使用方法tableSizeFor()來保證無論你給值是什么,回傳的一定是2的冪:

  static final int tableSizeFor(int cap)
    {
        int n = cap - 1; // 作用:保證當cap為2的冪時,回傳原值而不是二倍,如8 回傳8 而不是16
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

首先我們來看操作:

   		n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16

假設 n=01000000, n |= n >>> 1后 n=01100000,n |= n >>> 2后n=01111000,n |= n >>> 4;后n=01111111,我們可以發現,上述5步操作可以將一個32位數第一位為1的后面所有位全變為1,這樣再執行n + 1操作后,該數就必為2的冪次方了,如01111111+1 = 10000000,
那又為什么要保證一定是2的冪次方呢?不是不行嗎?

3.3.1 HashMap為何要保證容量始終是2的冪

說到這個問題不得不說執行put()方法時,是如何根據hash值在table中定位,

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
    {
		......
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        ......

可以看到,它使用了一個 (n - 1) & hash的操作,n為當前hashmap的容量,而容量一定為2的冪次方,n-1的二進制低位都為1,舉例:16=0000000000010000,15=0000000000001111,這樣的處理好處在于,當執行(n - 1) & hash的操作時,元素的位置僅取決于低位而與高位無關(這種無關性隨著HashMap容量的增大而減小),這個邏輯優點是,無論你的hash值有多大,我都鎖定了你的取值范圍小于當前容量,這樣做避免了hash值過于離散的情況,而當HashMap擴容時又可以同時增大hash值的取值范圍,缺點是增加了hash碰撞的可能性,為了解決這個問題HashMap修改了hash值的計算方法來增加低位的hash復雜度,

3.3.2 HashMap計算hash值

不廢話,直接上原始碼:

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

hash方法用 key的hash值異或上key的hash值的高16位,為什么要這樣做呢?
首先,h>>>16 的值高16位都為0,這樣h^(h>>>16)時,高16位的值不會有任何變化,但是低16位的值混雜了key的高16位的值,從而增加了hash值的復雜度,進一步減少了hash值一樣的概率,

3.4 HashMap為什么是執行緒不安全的

在Jdk1.7中,造成HashMap執行緒不安全的原因之一是transfer函式,該函式使用頭查法在多執行緒的情況下很容易出現倍訓鏈表從而導致死回圈,同時還有資料丟失的問題,Jdk1.8中沒有transfer函式而是在resize函式中完成了HashMap擴容或者初始化操作,resize采用尾插法很好的解決了倍訓鏈表的問題,但是依舊避免不了資料覆寫的問題,
在HashMap的put操作中:

  final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
    {
        Node<K, V>[] tab;
        Node<K, V> p;
        int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        ......

在執行完 if ((tab = table) == null || (n = tab.length) == 0)判斷且為true的情況下,會直接進行賦值,但是在多執行緒的環境下,當兩個執行緒同時完成判斷,執行緒1剛賦值完,執行緒2再進行賦值,就造成了資料覆寫的問題,
這只是最簡單的現象,我們要想執行緒安全,首先要有多執行緒安全的處理邏輯,很明顯HashMap是沒有這樣的邏輯的,那么很多為單執行緒設計的邏輯就很大可能出問題,所以HashMap為什么是執行緒不安全的?它本身設計就不支持多執行緒下的操作,所以不該有此問,
如果想要執行緒安全的使用基于hash表的map,可以使用ConcurrentHashMap,該實作get操作是無鎖的,put操作也是分段鎖,性能很好,
所以說術業有專攻,每個容器的實作都有它對應的優缺點,我們需要學會的是分析面對的每一種情況,合理的使用不同的容器去解決問題,

HashMap基本的原理和對應實作就說到這里了,更深入的話題如:紅黑樹插入節點、平衡紅黑樹、遍歷紅黑樹,可以直接看紅黑樹對應的原理和實作,

需要原始碼注釋的請戳這里原始碼決議


公眾號:良許Linux

有識訓?希望老鐵們來個三連擊,給更多的人看到這篇文章

轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/27112.html

標籤:Linux

上一篇:安裝ubuntu虛擬機及布署C#開發環境步驟

下一篇:【菜鳥版】Linux系統擴容根目錄磁盤空間的操作方法/多塊硬碟掛載到同一目錄

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • CA和證書

    1、在 CentOS7 中使用 gpg 創建 RSA 非對稱密鑰對 gpg --gen-key #Centos上生成公鑰/密鑰對(存放在家目錄.gnupg/) 2、將 CentOS7 匯出的公鑰,拷貝到 CentOS8 中,在 CentOS8 中使用 CentOS7 的公鑰加密一個檔案 gpg -a ......

    uj5u.com 2020-09-10 00:09:53 more
  • Kubernetes K8S之資源控制器Job和CronJob詳解

    Kubernetes的資源控制器Job和CronJob詳解與示例 ......

    uj5u.com 2020-09-10 00:10:45 more
  • VMware下安裝CentOS

    VMware下安裝CentOS 一、軟硬體準備 1 Centos鏡像準備 1.1 CentOS鏡像下載地址 下載地址 1.2 CentOS鏡像下載程序 點擊下載地址進入如下圖的網站,選擇需要下載的版本,這里選擇的是Centos8,點擊如圖所示。 決定選擇Centos8后,選擇想要的鏡像源進行下載,此 ......

    uj5u.com 2020-09-10 00:12:10 more
  • 如何使用Grep命令查找多個字串

    如何使用Grep 命令查找多個字串 大家好,我是良許! 今天向大家介紹一個非常有用的技巧,那就是使用 grep 命令查找多個字串。 簡單介紹一下,grep 命令可以理解為是一個功能強大的命令列工具,可以用它在一個或多個輸入檔案中搜索與正則運算式相匹配的文本,然后再將每個匹配的文本用標準輸出的格式 ......

    uj5u.com 2020-09-10 00:12:28 more
  • git配置http代理

    git配置http代理 經常遇到克隆 github 慢的問題,這里記錄一下幾種配置 git 代理的方法,解決 clone github 過慢。 目錄 git配置代理 git單獨配置github代理 git配置全域代理 配置終端環境變數 git配置代理 主要使用 git config 命令 git單獨 ......

    uj5u.com 2020-09-10 00:12:33 more
  • Linux npm install 裝包時提示Error EACCES permission denied解

    npm install 裝包時提示Error EACCES permission denied解決辦法 ......

    uj5u.com 2020-09-10 00:12:53 more
  • Centos 7下安裝nginx,使用yum install nginx,提示沒有可用的軟體包

    Centos 7下安裝nginx,使用yum install nginx,提示沒有可用的軟體包。 18 (flaskApi) [root@67 flaskDemo]# yum -y install nginx 19 已加載插件:fastestmirror, langpacks 20 Loading ......

    uj5u.com 2020-09-10 00:13:13 more
  • Linux查看服務器暴力破解ssh IP

    在公網的服務器上經常遇到別人爆破你服務器的22埠,用來挖礦或者干其他嘿嘿嘿的事情~ 這種情況下正確的做法是: 修改默認ssh的22埠 使用設定密鑰登錄或者白名單ip登錄 建議服務器密碼為復雜密碼 創建普通用戶登錄服務器(root權限過大) 建立堡壘機,實作統一管理服務器 統計爆破IP [root ......

    uj5u.com 2020-09-10 00:13:17 more
  • CentOS 7系統常見快捷鍵操作方式

    Linux系統中一些常見的快捷方式,可有效提高操作效率,在某些時刻也能避免操作失誤帶來的問題。 ......

    uj5u.com 2020-09-10 00:13:31 more
  • CentOS 7作業系統目錄結構介紹

    作業系統存在著大量的資料檔案資訊,相應檔案資訊會存在于系統相應目錄中,為了更好的管理資料資訊,會將系統進行一些目錄規劃,不同目錄存放不同的資源。 ......

    uj5u.com 2020-09-10 00:13:35 more
最新发布
  • vim的常用命令

    Vim的6種基本模式 1. 普通模式在普通模式中,用的編輯器命令,比如移動游標,洗掉文本等等。這也是Vim啟動后的默認模式。這正好和許多新用戶期待的操作方式相反(大多數編輯器默認模式為插入模式)。 2. 插入模式在這個模式中,大多數按鍵都會向文本緩沖中插入文本。大多數新用戶希望文本編輯器編輯程序中一 ......

    uj5u.com 2023-04-20 08:43:21 more
  • vim的常用命令

    Vim的6種基本模式 1. 普通模式在普通模式中,用的編輯器命令,比如移動游標,洗掉文本等等。這也是Vim啟動后的默認模式。這正好和許多新用戶期待的操作方式相反(大多數編輯器默認模式為插入模式)。 2. 插入模式在這個模式中,大多數按鍵都會向文本緩沖中插入文本。大多數新用戶希望文本編輯器編輯程序中一 ......

    uj5u.com 2023-04-20 08:42:36 more
  • docker學習

    ###Docker概述 真實專案部署環境可能非常復雜,傳統發布專案一個只需要一個jar包,運行環境需要單獨部署。而通過Docker可將jar包和相關環境(如jdk,redis,Hadoop...)等打包到docker鏡像里,將鏡像發布到Docker倉庫,部署時下載發布的鏡像,直接運行發布的鏡像即可。 ......

    uj5u.com 2023-04-19 09:26:53 more
  • 設定Windows主機的瀏覽器為wls2的默認瀏覽器

    這里以Chrome為例。 1. 準備作業 wsl是可以使用Windows主機上安裝的exe程式,出于安全考慮,默認情況下改功能是無法使用。要使用的話,終端需要以管理員權限啟動。 我這里以Windows Terminal為例,介紹如何默認使用管理員權限打開終端,具體操作如下圖所示: 2. 操作 wsl ......

    uj5u.com 2023-04-19 09:25:49 more
  • docker學習

    ###Docker概述 真實專案部署環境可能非常復雜,傳統發布專案一個只需要一個jar包,運行環境需要單獨部署。而通過Docker可將jar包和相關環境(如jdk,redis,Hadoop...)等打包到docker鏡像里,將鏡像發布到Docker倉庫,部署時下載發布的鏡像,直接運行發布的鏡像即可。 ......

    uj5u.com 2023-04-19 09:19:04 more
  • Linux學習筆記

    IP地址和主機名 IP地址 ifconfig可以用來查詢本機的IP地址,如果不能使用,可以通過install net-tools安裝。 Centos系統下ens33表示主網卡;inet后表示IP地址;lo表示本地回環網卡; 127.0.0.1表示代指本機;0.0.0.0可以用于代指本機,同時在放行設 ......

    uj5u.com 2023-04-18 06:52:01 more
  • 解決linux系統的kdump服務無法啟動的問題

    問題:專案麒麟系統服務器的kdump服務無法啟動,沒有相關日志無法定位問題。 1、查看服務狀態是關閉的,重啟系統也無法啟動 systemctl status kdump 2、修改grub引數,修改“crashkernel”為“512M(有的機器數值太大太小都會導致報錯,建議從128M開始試,或者加個 ......

    uj5u.com 2023-04-12 09:59:50 more
  • 解決linux系統的kdump服務無法啟動的問題

    問題:專案麒麟系統服務器的kdump服務無法啟動,沒有相關日志無法定位問題。 1、查看服務狀態是關閉的,重啟系統也無法啟動 systemctl status kdump 2、修改grub引數,修改“crashkernel”為“512M(有的機器數值太大太小都會導致報錯,建議從128M開始試,或者加個 ......

    uj5u.com 2023-04-12 09:59:01 more
  • 你是不是暴露了?

    作者:袁首京 原創文章,轉載時請保留此宣告,并給出原文連接。 如果您是計算機相關從業人員,那么應該經歷不止一次網路安全專項檢查了,你肯定是收到過資訊系統技術檢測報告,要求你加強風險監測,確保你提供的系統服務堅實可靠了。 沒檢測到問題還好,檢測到問題的話,有些處理起來還是挺麻煩的,尤其是線上正在運行的 ......

    uj5u.com 2023-04-05 16:52:56 more
  • 細節拉滿,80 張圖帶你一步一步推演 slab 記憶體池的設計與實作

    1. 前文回顧 在之前的幾篇記憶體管理系列文章中,筆者帶大家從宏觀角度完整地梳理了一遍 Linux 記憶體分配的整個鏈路,本文的主題依然是記憶體分配,這一次我們會從微觀的角度來探秘一下 Linux 內核中用于零散小記憶體塊分配的記憶體池 —— slab 分配器。 在本小節中,筆者還是按照以往的風格先帶大家簡單 ......

    uj5u.com 2023-04-05 16:44:11 more