1. 程式人生 > >JDK1.7和1.8中HashMap與ConcurrentHashMap總結比較

JDK1.7和1.8中HashMap與ConcurrentHashMap總結比較

談到HashMap和ConcurrentHashMap,必然會聯想到一些其他集合結構,比如HashTable,Vector等,先理一下他們的區別吧。其實HashTable和Vector已經被廢棄了,HashTable和Vector以及ConcurrentHashMap都是執行緒安全的同步結構,區別是HashTable和Vector是採用synchronized關鍵字對整個集合物件加鎖,效率低下。而ConcurrentHashMap則是採用分段加鎖的方式細化了鎖的顆粒度,由於高效率所以替代了前兩者。但是HashMap是執行緒不安全的,至於為什麼執行緒不安全下文會有詳解,先比較一下兩個版本中HashMap和ConcurrentHashMap有什麼改變。

一.HashMap

1.JDK1.7中

HashMap本質上是一個Entry陣列,Entry中即為鍵值對,Entry陣列在table中的位置是這麼計算出來的:

int hash = hash(key.hashCode());
int index = indexFor(hash,table.length);

大致是根據key物件的hashcode值,即key物件的記憶體地址值,通過hash演算法得到一個hash值,這裡很清晰的可以看出hashcode值和hash值是兩個東西兩個概念不要混淆。得到hash值後,再結合陣列table的長度計算出陣列下標,這裡貼出兩個演算法內容,即hash演算法hash()以及位置演算法indexFor()。

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

我也不太清楚這裡h>>16是什麼意思,不過ConcurrentHashMap預設執行緒併發量是16,是不是有關係呢?

static int indexFor(int h,int length){
    return h & (length - 1);
}

這裡有一個設計很巧妙的地方,關於為什麼HashMap的長度包括擴容後的長度都是2的次方這個問題,這個indexFor方法邏輯給出了答案。因為2的次方減一即(length - 1)得到的數其二進位制樣式差不多,前面有多少個0不一定,但後面一定是連續的1。比如(2-1)的二進位制是000001,(2的三次方-1)的二進位制是000111。而&(與運算)原則是有0就是0,都是1才是1。想象一下,比如000111這種二進位制參與與運算,前三位即0的位置不管另一個二進位制上對應位是什麼都是0,即一種情況,而後三位即1的那位得看另一個二進位制對應位置上是什麼,即兩種情況。很好理解,1越多,情況越多,即求出來的陣列下標可能性越多,即雜湊越均勻,形成的連結串列越少,查詢效率越高。理解這個小知識點很重要。

好像有點扯遠了,再回來看JDK1.7中HashMap的結構,通過前文也可以看出來,如果兩個key計算出來的陣列下標index相同,那麼這兩個Entry會在該位置上形成連結串列,結構可以這麼畫:

HashMap的get(key)方法就是根據key求出陣列下標,還是那一套,如果那個位置沒有連結串列,直接返回value,如果有連結串列就遍歷連結串列拿到value。這裡有個地方容易模糊,哪怕這個key在HashMap中不存在,也是可以根據這個key求出一個數組下標index的,只不過根據這個key拿value的時候為null。

HashMap的put方法就是根據負載因子和陣列長度先判斷需不需要擴容 ,注意,因為擴容後的table的長度就變了嘛,而每個Entry的陣列下標位置和table的長度有關,所以擴容後會重新計算這些Entry的陣列下標位置,可以理解為全部重新放了一遍,空間大了整理一下位置,整理好後就可以繼續往裡面PUT東西了,還是那套,先根據key計算出index值,判斷key存在不存在,存在就覆蓋不存在就新增。

這裡可以再補充一個知識點,關於為什麼HashMap不是執行緒安全的呢?因為put沒有加鎖,最終結果是某個位置上的Entry可能形成連結串列。知道多執行緒下有這個結果就行,至於為什麼會形成連結串列過程有點複雜暫且不論。如果某個位置上的Entry可能形成連結串列,get(key)的時候根據這個key計算出來的位置剛好是這個環形連結串列的位置,更不巧的是沒有這個key對應的鍵值對,那就完了,會一直遍歷這個環形連結串列,因為原始碼中是遍歷到null才推出迴圈的,環形連結串列沒有null結果就是死迴圈。這麼看來HashMap多執行緒下造成死迴圈的條件還是蠻苛刻的嘛。

2.JDK1.8中

JDK1.8中HashMap的結構發生了變化,即當Entry連結串列達到了一定的長度,會用紅黑樹結構代替連結串列結構,也就是說存在連結串列結構和紅黑樹結構同時存在的情況,圖可以這麼畫:

這樣就有效解決了Hash衝突問題,其他變化不大。

二.ConcurrentHashMap

1.JDK1.7中

ConcurrentHashMap能夠實現執行緒安全且高效是因為採用了分段加鎖的方式,與HashMap結合起來看,其實就是把一個大table分成了一段一段的Segment,Segment實現了再入鎖ReentranLock,即充當了ConcurrentHashMap中鎖的角色。Segment中有一個一個的HashEntry鍵值對有效資料,圖可以這麼畫:

其實可以把Segment理解為一個大table中一個一個的位置,這麼一理解ConcurrentHashMap與HashTable最大的區別就是ConcurrentHashMap對大table中每個位置加了鎖,而HashMap如果要加鎖的話就是對整個table加鎖,當然效率就高了。

ConcurrentHashMap的get方法還是那套,根據key找到對應的Segment,再遍歷key拿到具體的HashEntry。

ConcurrentHashMap的put方法就顯得複雜了,不過大致還是那套,大致是先判斷是否需要擴容,擴容整理後根據key找到對應的Segmentm,再往Segment中put鍵值對,這個時候put是加鎖的,利用自旋鎖去嘗試獲取鎖,獲取鎖後判斷key是否存在,存在就覆蓋不存在就新增一個鍵值對。總之就是利用再入鎖的方式鎖住Segment,保證只有一個執行緒在操作Segment,這就相當於在HashMap中保證了只有一個執行緒在陣列的一個位置中put,這當然不會形成環形連結串列了。

2.JDK1.8中

JDK1.8中變化較大,首先取消了Segment,理解一下第一層直接就是HashEntry。還有就是當連結串列達到一定長度的時候會以紅黑樹的形式代替,這個和HashMap一樣嘛,圖可以這麼畫:

put的時候採用了CAS+synchronized保證執行緒安全,get就還是那樣,讀不影響執行緒安全,所以變化不大。