日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

hashmap 存取原理图_HashMap底层实现原理

發布時間:2023/12/6 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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的性能。

創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎

總結

以上是生活随笔為你收集整理的hashmap 存取原理图_HashMap底层实现原理的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。