hashmap 存取原理圖_HashMap底層實現原理

HashMap底層原理總結,幾個Hash集合之間的對比。

HashMap底層存儲結構

HashMap是一個用于存儲Key-Value鍵值對的集合,每一個鍵值對也叫做一個Entry。這些Entry分散存儲在一個數組當中,這個數組就是HashMap的主干。1

2

3

4

5

6

7* The table, initialized on first use, and resized as

* necessary. When allocated, length is always a power of two.

* (We also tolerate length zero in some operations to allow

* bootstrapping mechanics that are currently not needed.)

*/

transient Node[] table;1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18* Basic hash bin node, used for most entries. (See below for

* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)

*/

static class implements Map.Entry{

final int hash;

final K key;

V value;

Node next;

Node(int hash, K key, V value, Node next) { ... }

public final K getKey(){ return key; }

public final V getValue(){ return value; }

public final String toString(){ return key + "=" + value; }

public final int hashCode(){ return Objects.hashCode(key) ^ Objects.hashCode(value);}

public final V setValue(V newValue){ ... }

public final boolean equals(Object o){ ... }

}

因為table數組的長度是有限的,再好的hash函數也會出現index沖突的情況,所以我們用鏈表來解決這個問題,table數組的每一個元素不只是一個Entry對象,也是一個鏈表的頭節點,每一個Entry對象通過Next指針指向下一個Entry節點。當新來的Entry映射到沖突數組位置時,只需要插入對應的鏈表即可。

需要注意的是:新來的Entry節點插入鏈表時,會插在鏈表的頭部,因為HashMap的發明者認為,后插入的Entry被查找的可能性更大。

HashMap中的table數組如下所示:

所以,HashMap是數組+鏈表+紅黑樹(在Java 8中為了優化Entry的查找性能,新加了紅黑樹部分)實現的。

Put方法原理

調用hashMap.put("str", 1),將會在HashMap的table數組中插入一個Key為“str”的元素,這時候需要我們用一個hash()函數來確定Entry的插入位置,而每種數據類型有自己的hashCode()函數,比如String類型的hashCode()函數如下所示:1

2

3

4

5

6

7public static int hashCode(byte[] value){

int h = 0;

for (byte v : value) {

h = 31 * h + (v & 0xff);

}

return h;

}

所以,put()函數的執行路徑是這樣的:首先put("str", 1)會調用HashMap的hash("str")方法。

在hash()內部,會調用String(Latin1)內部的hashcode()獲取字符串”str”的hashcode。

“str”的hashcode被返回給put(),put()通過一定計算得到最終的插入位置index。

最后將這個Entry插入到table的index位置。

這里就出現了兩個問題,問題1: 在put()里怎樣得到插入位置index?問題2: 為什么會調用HashMap的hash()函數,直接調用String的hashcode()不好嗎?

問題1: 在put()里怎樣得到插入位置index?

對于不同的hash碼我們希望它被插入到不同的位置,所以我們首先會想到對數組長度的取模運算,但是由于取模運算的效率很低,所以HashMap的發明者用位運算替代了取模運算。

在put()里是通過如下的語句得到插入位置的:1index = hash(key) & (Length - 1)

其中Length是table數組的長度。為了實現和取模運算相同的功能,這里要求(Length - 1)這部分的二進制表示全為1,我們用HashMap的默認初始長度16舉例說明:1

2

3

4

5假設"str"的hash嗎為: 1001 0110 1011 1110 1101 0010 1001 0101

Length - 1 = 15 : 1111

hash("str") & (Length - 1) = 0101

如果(Length - 1)這部分不全為1,假如Length是10,那么Length - 1 = 9 :1001 那么無論再和任何hash碼做與操作,中間兩位數都會是0,這樣就會出現大量不同的hash碼被映射到相同位置的情況。

所以,在HashMap中table數組的默認長度是16,并且要求每次自動擴容或者手動擴容時,長度都必須是2的冪。

問題2: 為什么會調用HashMap的hash()函數,直接調用String的hashcode()不好嗎?

HashMap中的hash()函數如下所示:1

2

3

4static final int hash(Object key){

int h;

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

HashMap中的hash()函數是將得到hashcode做進一步處理,它將hashcode的高16位和低16位進行異或操作,這樣做的目的是:在table的長度比較小的情況下,也能保證hashcode的高位參與到地址映射的計算當中,同時不會有太大的開銷。

綜上所述:從hashcode計算得到table索引的計算過程如下所示:

put()方法的執行過程如下所示:

HashMap的擴容機制

在HashMap中有一下兩個屬性和擴容相關:1

2int threshold;

final float loadFactor;

其中threshold = Length * loadFactor,Length表示table數組的長度(默認值是16),loadFactor為負載因子(默認值是0.75),閥值threshold表示當table數組中存儲的元素超過這個閥值的時候,就需要擴容了。以默認長度16,和默認負載因子0.75為例,threshold = 16 * 0.75 = 12,即當table數組中存儲的元素個數超過12個的時候,table數組就該擴容了。

當然Java中的數組是無法自動擴容的,方法是使用一個新的數組代替已有的容量小的數組,然后將舊數組中的元素經過重新計算放到新數組中,那么怎樣對舊元素進行重新映射呢?

其實很簡單,由于我們在擴容時,是使用2的冪擴展,即數組的長度擴大到原來的2倍, 4倍, 8倍…,因此在resize時(Length - 1)這部分相當于在高位新增一個或多個1bit,我們以擴大到原來的兩倍為例說明:

(a)中n為16,(b)中n擴大到兩倍為32,相當于(n - 1)這部分的高位多了一個1, 然后和原hash碼作與操作,這樣元素在數組中映射的位置要么不變,要不向后移動16個位置:

因此,我們在擴充HashMap的時候,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”,可以看看下圖為16擴充為32的resize示意圖:

這個設計確實非常的巧妙,既省去了重新計算hash值的時間,而且同時,由于新增的1bit是0還是1可以認為是隨機的,因此resize的過程,均勻的把之前的沖突的節點分散到新的bucket了。這一塊就是JDK1.8新增的優化點。有一點注意區別,JDK1.7中resize的時候,舊鏈表遷移新鏈表的時候,如果在新表的數組索引位置相同,則鏈表元素會倒置,但是從上圖可以看出,JDK1.8不會倒置。

HashMap死鎖形成原理

HashMap非線程安全,即任一時刻可以有多個線程同時寫HashMap,可能會導致數據的不一致。如果需要滿足線程安全,可以用 Collections的synchronizedMap方法使HashMap具有線程安全的能力,或者使用線程安全的ConcurrentHashMap。

要理解HashMap死鎖形成的原理,我們要對HashMap的resize里的transfer過程有所了解,transfer過程是將舊數組中的元素復制到新數組中,在Java 8之前,復制過程會導致鏈表倒置,這也是形成死鎖的重要原因(Java 8中已經不會倒置),transfer的基本過程如下:1

2

3

41. 新建節點e指向當前節點,新建節點next指向e.next

2. 將e.next指向新數組中指定位置newTable[i]

3. newTable[i] = e

4. e = next

舉個例子:1

2

3

4

5

6

7現在有鏈表1->2->3,要將它復制到新數組的newTable[i]位置

1. Node e = 1, next = e.next;

2. e.next = newTable[i];

3. newTable[i] = e;

4. e = next, next = e.next;

執行完后會得到這樣的結果:

newTable[i]=3->2->1

死鎖會在這種情況產生:兩個線程同時往HashMap里放Entry,同時HashMap正好需要擴容,如果一個線程已經完成了transfer過程,而另一個線程不知道,并且又要進行transfer的時候,死鎖就會形成。1

2

3

4

5現在Thread1已將完成了transfer,newTable[i]=3->2->1

在Thread2中:

Node e = 1, next = e.next;

e.next = newTable[i] : 1 -> newTable[i]=3

newTable[i] = e : newTable[i] = 1->3->2->1 //這時候鏈表換已經形成了

在形成鏈表換以后再對HashMap進行Get操作時,就會形成死循環。

在Java 8中對這里進行了優化,鏈表復制到新數組時并不會倒置,不會因為多個線程put導致死循環,但是還有很多弊端,比如數據丟失等,因此多線程情況下還是建議使用ConcurrentHashMap。

HashMap和Hashtable有什么區別

Java為數據結構中的映射定義了一個接口java.util.Map,此接口主要有四個常用的實現類,分別是HashMap、Hashtable、LinkedHashMap和TreeMap,類繼承關系如下圖所示:

Hashtable:Hashtable是遺留類,很多映射的常用功能與HashMap類似,不同的是它承自Dictionary類,并且是線程安全的,任一時間只有一個線程能寫Hashtable,并發性不如ConcurrentHashMap,因為ConcurrentHashMap引入了分段鎖。Hashtable不建議在新代碼中使用,不需要線程安全的場合可以用HashMap替換,需要線程安全的場合可以用ConcurrentHashMap替換。

總結擴容是一個特別耗性能的操作,所以當程序員在使用HashMap的時候,估算map的大小,初始化的時候給一個大致的數值,避免map進行頻繁的擴容。

負載因子是可以修改的,也可以大于1,但是建議不要輕易修改,除非情況非常特殊。

HashMap是線程不安全的,不要在并發的環境中同時操作HashMap,建議使用ConcurrentHashMap。

JDK1.8引入紅黑樹大程度優化了HashMap的性能。

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

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

相關文章

LVM邏輯卷創建管理

在虛擬機中再次添加三張硬盤 1、查看添加的硬盤 [rootrhel-02 ~]# fdisk -l 2、添加分區 [rootrhel-02 ~]# fdisk /dev/sdb 查看分區并保存 3、將物理硬盤分區初始化為物理卷,以便LVM使用 如果沒安裝LVM的話先去安裝 [rootrhel-02 ~]# yum install lvm2 安裝完成…

Start DWM manually on Windows 7 and vista

方法一: 1. 檢查兩處注冊表項及鍵值是否與下列數值一致 HKEY-Current-User\Software\Microsoft\Windows\DWM\Composition 鍵值改為 1 HKEY-Current-User\Software\Microsoft\Windows\DWM\CompositionPolicy 鍵值改為2 2. 打開運行(可能要用到管理員模式啟…

java啟動mysq服務_Java Web開發——MySQL數據庫的安裝與配置

MySQL是一個關系型數據庫管理系統,由瑞典MySQL AB 公司開發,目前屬于 Oracle 旗下產品。MySQL 是最流行的關系型數據庫管理系統之一,在 WEB 應用方面,MySQL是最好的 RDBMS (Relational Database Management System,關系…

小程序如何獲得手機號碼_獲得小型企業電話號碼的最佳方法

小程序如何獲得手機號碼Lots of small businesses use their personal cellphones when making work related phone calls. Some may even be using old landlines for their calling needs. While it makes sense to use your cellphone, and it can be scary to make a chang…

空間數據索引RTree完全解析及Java實現

版權聲明:本文為博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/MongChia1993/article/details/69941783第一部分 空間數據的背景介紹 空間數據的建模 基于實體的模型(基于對象)Entity-based models (or object base…

Android 中的ORM框架

在android 中,內置了sqlite數據庫,java web 中,用慣了Hibernate ,想找找android中是否也有類似的orm框架,后來在開源中國看到了orman,這是一個很不錯的框架。 這個可以幫我們快捷方便的實現數據庫的CURD操作…

android頁面布局 如何讓中間的listview填充剩余部分_谷歌駕駛設計—界面設計布局...

本節提供了可在不同屏幕尺寸范圍內縮放的屏幕布局的設計指南。此處定義的padding和keyline值用于Components,Media規范、Notification Center規范和Dialer規范中。指南概覽(TL:DR):基于適當的屏幕尺寸類別的基本布局使…

ios 禁用滑動手勢_如何禁用筆記本電腦上的Windows 8滑動手勢?

ios 禁用滑動手勢If you’re not a fan of the touchpad-based swipe gestures in Windows 8 there is a way to completely disable them and reclaim your touchpad. 如果您不喜歡Windows 8中基于觸摸板的滑動手勢,可以使用一種方法來完全禁用它們并收回您的觸摸板…

Java快速入門-01-基礎篇

Java快速入門-01-基礎篇 如果基礎不好或者想學的很細,請參看:菜鳥教程-JAVA本筆記適合快速學習,文章后面也會包含一些常見面試問題,記住快捷鍵操作,一些內容我就不轉載了,直接附上鏈接,嘻嘻開發…

Excel導入MS SQL SERVER 操作

關于Excel導入到sql操作的相關問題總結: 一、大批量數據導入 方法1、從Excel大批量數據導入時我們可以使用sql里面有一個batch copy的功能 方法2、在sql中建一個table type結構,在前端將excel讀到datatable中,把整個datatable作為存儲過程參數…

蘋果mac閃退_自從Mac有了WPS,從此和雙系統說再見!

薛崗13,712本文共計2266個字,預計閱讀時長需要6分鐘。大部分使用Macbook的用戶都有一個痛點,就是編輯好的office文件,在朋友或同事的windows電腦上展示效果與自己的會有差異。除此外,卡頓、閃退、數據丟失等也是Windows版office在…

初學者計算機_初學者極客:如何在計算機上重新安裝Windows

初學者計算機Reinstalling Windows is one of the easiest ways to fix software problems on your computer, whether it’s running slow or infected by viruses. You should also reinstall Windows before you get rid of an old PC. 重新安裝Windows是修復計算機上軟件問…

win7 32位 安裝opencv-python后,運行時提示 from .cv2 import *: DLL load failed: 找不到指定的模塊 的解決辦法...

安裝opencv后,運行一個測試程序提示"from .cv2 import *: DLL load failed: 找不到指定的模塊"。于是百度一下解決辦法,結果試了N多方法后也沒能解決這個問題。 最后不得不耐心的下載了dependency walker來查看opencv到底是缺少了哪個dll文件。…

goahead處理json_GoAhead Web Server遠程代碼執行漏洞分析(附PoC)

*本文中涉及到的相關漏洞已報送廠商并得到修復,本文僅限技術研究與討論,嚴禁用于非法用途,否則產生的一切后果自行承擔。本文是關于GoAhead web server遠程代碼執行漏洞(CVE-2017-17562)的分析,該漏洞源于在初始化CGI腳本環境時使…

項目中的模塊剝離成項目_使用MCEBuddy 2從電視錄制中剝離廣告

項目中的模塊剝離成項目One of the great things about time-shifting your television viewing is that you are able to watch the shows you love at a time that suits you. Just because you have an appointment on Wednesday evening there’s no need to miss out on y…

有上下界限制可行流

無源匯有上下界限制可行流(循環流) 即每條邊的流量限制為[L,R],判斷有沒有滿足整個網絡的可行流。 看看以前學的網絡流,實際上它的流量限制為[0,C],現在無非多了一個下限的限制。 網絡流的一個重要性質:除了…

.gitignore文件將已經納入版本管理的文件刪除

git rm -r --cached . git add . git commit -m update .gitignore git push -u origin master 先將本地緩存刪除,再提交,.gitignore文件只針對那些沒有被staged的文件有效 參考博客:https://www.cnblogs.com/kevingrace/p/5690241.html 轉載…

gmail收件箱標簽設置_通過在Gmail中啟用實驗室功能來啟動收件箱

gmail收件箱標簽設置We recently looked at how you can make it easier to manage multiple inboxes in Gmail using the Multiple Inboxes Lab feature. This is a non-standard feature and it’s far from being the only one available to you. In fact there are numerou…

linux rmp命令安裝包在哪里_rpm命令_Linux rpm 命令用法詳解:RPM軟件包的管理工具...

rpm命令是RPM軟件包的管理工具。rpm原本是Red Hat Linux發行版專門用來管理Linux各項套件的程序,由于它遵循GPL規則且功能強大方便,因而廣受歡迎。逐漸受到其他發行版的采用。RPM套件管理方式的出現,讓Linux易于安裝,升級&#xf…

【題解】洛谷P1066 [NOIP2006TG] 2^k進制數(復雜高精+組合推導)

洛谷P1066:https://www.luogu.org/problemnew/show/P1066 思路 挺難的一道題 也很復雜 滿足題目要求的種數是兩類組合數之和 r的最多位數m為 w/k(當w mod k0 時)w/k1(當 w mod k1 時)First: 位數為2~m的種數 即從2k-1中…