HashMap底層原理

2022-09-09 08:39:10 字數 4044 閱讀 2741

hashmap底層資料結構是由陣列鍊錶紅黑樹組成的,紅黑樹是jdk1.8之後加入的,當鍊表長度超過8時,並且陣列長度大於64,鍊錶就會轉化成紅黑樹。

hashmap中的每乙個元素,也就是鍵值對node物件(key,value,hash,next),都是通過put()get()方法來儲存和獲取的。

儲存物件時,呼叫put()方法,把鍵和值傳給put()方法,如果容器中的元素個數大於設定的容量*擴容因子,就會呼叫resize()方法進行擴容。然後呼叫hash(k)計算鍵的hashcode,根據hashcode和陣列長度計算陣列下標index,index=(length - 1)&hash

如果鍵的hashcode沒有發生hash衝突,則根據計算的陣列下標直接插入到陣列中;

如果發生了hash衝突,就會呼叫equals()方法,對比鍵值是否相等,返回ture則替換原來的鍵值對。如果返回false,就會把鍵值對插入鍊錶,jdk1.8採用的是尾插法,之前的版本採用的是頭插法,如果是紅黑樹,則採用紅黑樹的插法。

獲取物件時,呼叫get()方法,計算key的hashcode值,如果沒有發生hash衝突,就直接到陣列取資料,如果發生了hash衝突,就呼叫equals()方法對比key,到鍊錶或者紅黑樹中取查詢物件。

hashmap的預設初始容器大小是16,預設擴容因子是0.75

當我們指定容器大小size時,會呼叫tablesizefor方法對容器大小進行初始化,過程是現將n=size-1,

然後n |=n>>>1;n |=>>>2; n |=>>>4; n|=>>>8; n|=>>>16;最後算出size=n+1;(依次或上自己的無符號右移1、2、4、8、16位)

如果在new hashmap 時沒有指定大小:

在建立的時候並沒有進行陣列的初始化,table陣列為null,在第一次put操作的時候,執行resize(),進行的初始化。

在理想情況下,使用隨機雜湊碼,節點出現的頻率在hash桶中遵循泊松分布。為了減少衝突的概率。

如果hash值的第三位更加隨機那麼結果就更加隨機,那麼如何讓低三位更加隨機 那麼就是讓hashcode與hashcode的的無符號右移16位進行異或運算

static final int hash(object key)
如果hashmap內的元素個數大於容器大小*擴容因子,就會發生擴容

建立乙個新的陣列,其容量為舊陣列的兩倍,並重新計算舊陣列中結點的儲存位置。結點在新陣列中的位置只有兩種,原下標位置或原下標+舊陣列的大小。

resize()方法中,先把舊陣列的所有資料移到新陣列中,然後巧妙地使用了(node.hash & oldsize)=0,把陣列從新分為兩類,一類是位置沒有發生改變的,另一類是位置發生改變的,當然,前提還是陣列的長度是2的n次方

① 等於0時,則將該頭節點放到新陣列時的索引位置等於其在舊陣列時的索引位置,記為低位區鍊錶lo開頭-low;

② 不等於0時,則將該頭節點放到新陣列時的索引位置等於其在舊陣列時的索引位置再加上舊陣列長度,記為高位區鍊錶hi開頭high.

紅黑樹的節點非黑即紅,根節點一定是黑的,紅黑樹根節點左邊的數一定小於右邊的數,每個紅節點的兩個子節點一定是黑的,任意一結點到每個葉子結點的路徑都包含數量相同的黑結點。。

因為鍊錶邊長了之後,遍歷速度會急劇下降,所以jdk1.8之後引入了紅黑樹,因為紅黑樹的特點,它的遍歷速度相對來說比煉表快了很多,而且插入元素的速度也不慢,大大提高了查詢效能,平均查詢長度從o(n)降低到o(logn)

鍊錶在長度大於8並且hashmap的size大於64時,就會把鍊錶轉化成紅黑樹。當size小於6時,紅黑樹會換成鍊錶

q: 為什麼是8?

a:因為鍊錶的平均查詢長度是n/2,紅黑樹的平均查詢長度是log(n);

等於8時:8/2=4;log(8)=3紅黑樹比煉表快;

等於6時:6/2=3;log(6)=2.6;雖然這時紅黑樹也比鍊錶快,但是這時會導致鍊錶和紅黑樹之間頻繁轉化,導致效能下降,得不償失,所以8最合適。

在多執行緒環境下,hashmap在put元素時,容易發生執行緒不安全,jdk1.7在擴容resize()時可能會形成環鏈,因為發生hash衝突時,jdk1.7採用的是頭插法,在擴容的時候,resize()的鍊錶插入也是用的頭插法,同一位置上的新元素會被放在最頭部,假如有兩個entry,a和b,a.next->b;在擴容時,多執行緒操作下可能b插入到a頭部,b.next->a,這樣就形成了環鏈。jdk1.8採用尾插法,在put元素時,多執行緒可能執行到同一行**,都已經計算完hash,這兩條資料的hash值一樣,並且下乙個元素是空時,同時往裡面插資料,導致資料被覆蓋。所以hashmap不能保證執行緒安全,如果要在多執行緒下使用,建議使用concurrenthashmap。

hashmap和hashtable

在jdk1.7時,底層都是採用陣列加鍊表來實現的,區別在於多執行緒環境下,hashtable使用了synchronized關鍵字,來鎖住整個物件,保證執行緒安全。在jdk1.8之後,hashmap底層做了一些優化,加入了紅黑樹,但還是執行緒不安全的。

hashmap和concurrenthashmap

底層實現基本相同,就是在多執行緒環境下,concurrenthashmap jdk1.8採用了cas+synchronized關鍵字來實現鎖分段技術保證執行緒安全。

concurrenthashmap和hashtable

hashtable使用synchronized關鍵字來鎖住整個物件,當乙個執行緒在對hashtable進行put操作時,其他執行緒會來競爭鎖,只能等到上乙個執行緒釋放鎖,這樣效率低下,基本已經被拋棄使用。

在jdk1.7時,concurrenthashmap採用的是陣列加鍊表,對整個桶陣列進行了分割分段(segment),每一把鎖只鎖容器其中一部分資料,多執行緒訪問容器裡不同資料段的資料,就不會存在鎖競爭,提高併發訪問率。

在jdk1.8,加入了紅黑樹,並且採用了cas和synchronized關鍵字來實現執行緒安全。而是使用synchronized鎖住了對應桶節點,而且是只有put和remove的時候才會上鎖,get的時候是不需要鎖的。如果是鍊錶,鎖住的是頭結點,如果是紅黑樹,鎖住的是整棵樹。

總結一下:就是concurrenthashmap結合了hashmap和hashtable兩者的優勢,既保證了效能高,有實現了效能安全。而hashmap只有效能高,沒有執行緒安全。所以一般情況下,我們會優先使用hashmap,在考慮到併發的環境下,使用concurrenthashmap。

q:什麼是cas? compare and swap

cas演算法的過程是這樣:它包含三個引數cas(v,e,n)。v表示要更新的變數,e表示預期值,n表示新值。僅當v值等於e值時,才會將v的值設為n,如果v值和e值不同,則說明已經有其他執行緒做了更新,則當前執行緒什麼都不做。

concurrenthashmap不允許鍵值為空。

concurrenthashmap的預設併發數是16,可以通過建構函式設定,當我們設定了併發數n後,實際的併發數會是大於n的2的冪次方數,例如設定15,實際是16。

concurrenthashmap的鎖鎖住的只是鍊錶和紅黑樹的根節點。

HashMap底層原理

1.hashmap概述 hashmap是基於雜湊表的map介面的非同步實現。此實現提供所有可選的對映操作,並允許使用null值和null鍵。此類不保證對映的順序,特別是它不保證該順序恆久不變。2.hashmap的資料結構 注意,迭代器的快速失敗行為不能得到保證,一般來說,存在非同步的併發修改時,不可...

HashMap底層原理

hashmap實現map介面,非執行緒安全的,區別於concurrenthashmap。允許使用null值和null鍵,不保證對映的順序.底層資料結構是乙個 陣列 鍊錶 紅黑樹 put 根據key計算得到key.hash h k.hashcode h 16 根據key.hash計算得到桶陣列的索引i...

HashMap底層原理

預設負載因子 static final float default load factor 0.75f 無參構造 public hashmap 有參構造 public hashmap int initialcapacity public hashmap int initialcapacity,flo...