1. 程式人生 > >hashmap實現原理(雜湊值計算,put方法,擴容) jdk1.8帶來的優化 hashmap併發安全 ConcurrentHashMap

hashmap實現原理(雜湊值計算,put方法,擴容) jdk1.8帶來的優化 hashmap併發安全 ConcurrentHashMap

HashMap的原始碼,實現原理,JDK8中對HashMap做了怎樣的優化。

ArrayList和LinkedList的優缺點——陣列的特點是:定址容易,插入和刪除困難;而連結串列的特點是:定址困難,插入和刪除容易。

hashmap底層是由陣列加連結串列構成,將需要存放的元素算出hash值,在對陣列長度取餘,得到下標。因為取餘得到的值,可能相同。於是該值為下標,存放一節連結串列,該連結串列存放key,value,next

系統將呼叫”美團”這個key的hashCode()方法得到其hashCode 值(該方法適用於每個Java物件),然後再通過Hash演算法的後兩步運算(高位運算和取模運算,下文有介紹)來定位該鍵值對的儲存位置,有時兩個key會定位到相同的位置,表示發生了Hash碰撞。當然Hash演算法計算結果越分散均勻,Hash碰撞的概率就越小,map的存取效率就會越高。

如果雜湊桶陣列很大,即使較差的Hash演算法也會比較分散,如果雜湊桶陣列陣列很小,即使好的Hash演算法也會出現較多碰撞,所以就需要在空間成本和時間成本之間權衡,其實就是在根據實際情況確定雜湊桶陣列的大小,並在此基礎上設計好的hash演算法減少Hash碰撞。那麼通過什麼方式來控制map使得Hash碰撞的概率又小,雜湊桶陣列(Node[] table)佔用空間又少呢?答案就是好的Hash演算法和擴容機制

瞭解下HashMap的幾個欄位。從HashMap的預設建構函式原始碼可知,建構函式就是對下面幾個欄位進行初始化

int threshold;             // 所能容納的key-value對極限 
final float loadFactor;    // 負載因子
int modCount;  
int size;

首先,Node[] table的初始化長度length(預設值是16)Load factor為負載因子(預設值是0.75),threshold是HashMap所能容納的最大資料量的Node(鍵值對)個數。threshold = length * Load factor。也就是說,在陣列定義好長度之後,負載因子越大,所能容納的鍵值對個數越多。

結合負載因子的定義公式可知,threshold就是在此Load factor和length(陣列長度)對應下允許的最大元素數目,超過這個數目就重新resize(擴容),擴容後的HashMap容量是之前容量的兩倍。預設的負載因子0.75是對空間和時間效率的一個平衡選擇,建議大家不要修改,除非在時間和空間比較特殊的情況下,如果記憶體空間很多而又對時間效率要求很高,可以降低負載因子Load factor的值

;相反,如果記憶體空間緊張而對時間效率要求不高,可以增加負載因子loadFactor的值,這個值可以大於1。

size這個欄位其實很好理解,就是HashMap中實際存在的鍵值對數量。注意和table的長度length、容納最大鍵值對數量threshold的區別。而modCount欄位主要用來記錄HashMap內部結構發生變化的次數,主要用於迭代的快速失敗。強調一點,內部結構發生變化指的是結構發生變化,例如put新鍵值對,但是某個key對應的value值被覆蓋不屬於結構變化。

在HashMap中,雜湊桶陣列table的長度length大小必須為2的n次方(一定是合數),這是一種非常規的設計,常規的設計是把桶的大小設計為素數。相對來說素數導致衝突的概率要小於合數,具體證明可以參考http://blog.csdn.net/liuqiyao_01/article/details/14475159,Hashtable初始化桶大小為11,就是桶大小設計為素數的應用(Hashtable擴容後不能保證還是素數)。HashMap採用這種非常規設計,主要是為了在取模和擴容時做優化,同時為了減少衝突,HashMap定位雜湊桶索引位置時,也加入了高位參與運算的過程。

key獲取雜湊桶陣列索引位置

方法一:
static final int hash(Object key) {   //jdk1.8 & jdk1.7
     int h;
     // h = key.hashCode() 為第一步 取hashCode值
     // h ^ (h >>> 16)  為第二步 高位參與運算
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
方法二:
static int indexFor(int h, int length) {  //jdk1.7的原始碼,jdk1.8沒有這個方法,但是實現原理一樣的
     return h & (length-1);  //第三步 取模運算
}

Hash演算法本質上就是三步:取key的hashCode值、高位運算、取模運算。

對於任意給定的物件,只要它的hashCode()返回值相同,那麼程式呼叫方法一所計算得到的Hash碼值總是相同的。我們首先想到的就是把hash值對陣列長度取模運算,這樣一來,元素的分佈相對來說是比較均勻的。但是,模運算的消耗還是比較大的,在HashMap中是這樣做的:呼叫方法二來計算該物件應該儲存在table陣列的哪個索引處。 

這個方法非常巧妙,它通過h & (table.length -1)來得到該物件的儲存位,而HashMap底層陣列的長度總是2的n次方,這是HashMap在速度上的優化。當length總是2的n次方時,h& (length-1)運算等價於對length取模,也就是h%length,但是&比%具有更高的效率。

在JDK1.8的實現中,優化了高位運算的演算法,通過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質量來考慮的,這麼做可以在陣列table的length比較小的時候,也能保證考慮到高低Bit都參與到Hash的計算中,同時不會有太大的開銷。

HashMap的put方法

①.判斷鍵值對陣列table[i]是否為空或為null,否則執行resize()進行擴容;

②.根據鍵值key計算hash值得到插入的陣列索引i,如果table[i]==null,直接新建節點新增,轉向⑥,如果table[i]不為空,轉向③;

③.判斷table[i]的首個元素是否和key一樣,如果相同直接覆蓋value,否則轉向④,這裡的相同指的是hashCode以及equals;

④.判斷table[i] 是否為treeNode,即table[i] 是否是紅黑樹,如果是紅黑樹,則直接在樹中插入鍵值對,否則轉向⑤;

⑤.遍歷table[i],判斷連結串列長度是否大於8,大於8的話把連結串列轉換為紅黑樹,在紅黑樹中執行插入操作,否則進行連結串列的插入操作;遍歷過程中若發現key已經存在直接覆蓋value即可;

⑥.插入成功後,判斷實際存在的鍵值對數量size是否超多了最大容量threshold,如果超過,進行擴容。

連結串列會將陣列某個節點上多出的元素按照尾插法(jdk1.7及以前為頭插法)的方式新增。

經過觀測可以發現,我們使用的是2次冪的擴充套件(指長度擴為原來2倍),所以,元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置。

我們在擴充HashMap的時候,不需要像JDK1.7的實現那樣重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”,

優化:

一旦出現拉鍊過長,引入了紅黑樹。而當連結串列長度太長(預設超過8)時,連結串列就轉換為紅黑樹 利用紅黑樹快速增刪改查的特點提高HashMap的效能

1.引入紅黑樹  2.優化了高位運算的演算法 計算hash值  3.擴容時,不需要重新計算hash

小結

(1) 擴容是一個特別耗效能的操作,所以當程式設計師在使用HashMap的時候,估算map的大小,初始化的時候給一個大致的數值,避免map進行頻繁的擴容。

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

(3) HashMap是執行緒不安全的,不要在併發的環境中同時操作HashMap,建議使用ConcurrentHashMap。

(4) JDK1.8引入紅黑樹大程度優化了HashMap的效能。

HashMap,HashTable,ConcurrentHashMap的區別

HashTable 底層陣列+連結串列實現,無論key還是value都不能為null,執行緒安全,實現執行緒安全的方式是在修改資料時鎖住整個HashTable,效率低,ConcurrentHashMap做了相關優化 初始size為11,擴容:newsize = olesize*2+1 計算index的方法:index = (hash & 0x7FFFFFFF) % tab.length HashMap 底層陣列+連結串列實現,可以儲存null鍵和null值,執行緒不安全 初始size為16,擴容:newsize = oldsize*2,size一定為2的n次冪 擴容針對整個Map,每次擴容時,原來陣列中的元素依次重新計算存放位置,並重新插入 插入元素後才判斷該不該擴容,有可能無效擴容(插入後如果擴容,如果沒有再次插入,就會產生無效擴容) 當Map中元素總數超過Entry陣列的75%,觸發擴容操作,為了減少連結串列長度,元素分配更均勻 計算index方法:index = hash & (tab.length – 1)

ConcurrentHashMap 底層採用分段的陣列+連結串列實現,執行緒安全 通過把整個Map分為N個Segment,可以提供相同的執行緒安全,但是效率提升N倍,預設提升16倍。(讀操作不加鎖,由於HashEntry的value變數是 volatile的,也能保證讀取到最新的值。) Hashtable的synchronized是針對整張Hash表的,即每次鎖住整張表讓執行緒獨佔,ConcurrentHashMap允許多個修改操作併發進行,其關鍵在於使用了鎖分離技術 有些方法需要跨段,比如size()和containsValue(),它們可能需要鎖定整個表而而不僅僅是某個段,這需要按順序鎖定所有段,操作完畢後,又按順序釋放所有段的鎖 擴容:段內擴容(段內元素超過該段對應Entry陣列長度的75%觸發擴容,不會對整個Map進行擴容),插入前檢測需不需要擴容,有效避免無效擴容 ConcurrentHashMap預設將hash表分為16個桶,諸如get、put、remove等常用操作只鎖住當前需要用到的桶。這樣,原來只能一個執行緒進入,現在卻能同時有16個寫執行緒執行,併發效能的提升是顯而易見的。

HashMap在高併發下如果沒有處理執行緒安全會有怎樣的安全隱患,具體表現是什麼

1、多執行緒put時可能會導致get無限迴圈,具體表現為CPU使用率100%; 原因:在向HashMap put元素時,會檢查HashMap的容量是否足夠,如果不足,則會新建一個比原來容量大兩倍的Hash表,然後把陣列從老的Hash表中遷移到新的Hash表中,遷移的過程就是一個rehash()的過程,多個執行緒同時操作就有可能會形成迴圈連結串列,所以在使用get()時,就會出現Infinite Loop的情況

2多執行緒put時可能導致元素丟失 原因:當多個執行緒同時執行addEntry(hash,key ,value,i)時,如果產生雜湊碰撞,導致兩個執行緒得到同樣的bucketIndex去儲存,就可能會發生元素覆蓋丟失的情況