HashMap 和 ConcurrentHashMap比較


基礎知識:

1. ConcurrentHashMap:

    (JDK1.7) segment數組,分段鎖;segment 內部是 HashEnty數組,類似HashMap;

                        統計長度的方法,先不加鎖統計兩次,如果一樣即為長度,否則加鎖,重新統計。先采用不加鎖的方式,連續計算元素的個數,最多計算3次:如果前后兩次計算結果相同,則說明計算出來的元素個數是准確的;

                        兩倍的方式擴充。方便擴容時數據移動,數據移動位置一般是當前位置或者 當前位置+原數組長度(增加的長度,根據是0或者1來判斷,更高效。

                        tryLock獲取鎖,獲取不到調用scanAndLockForPut()嘗試,嘗試64次后還不行,則掛起。等待線程釋放鎖,喚醒。

     (JDK1.8)放棄了Segment臃腫的設計,取而代之的是采用Node + CAS + Synchronized來保證並發安全進行實現。

                        如果相應位置的Node不為空,且當前該節點不處於移動狀態,則對該節點加synchronized鎖,如果該節點的hash不小於0,則遍歷鏈表更新節點或插入新節點;

                        如果該節點是TreeBin類型的節點,說明是紅黑樹結構,則通過putTreeVal方法往紅黑樹中插入節點;

                        如果binCount不為0,說明put操作對數據產生了影響,如果當前鏈表的個數達到8個,則通過treeifyBin方法轉化為紅黑樹,如果oldVal不為空,說明是一次更新操作,沒有對元素個數產生影響,則直接返回舊值

                        如果插入的是一個新節點,則執行addCount()方法嘗試更新元素個數baseCount;

                        1.8中使用一個volatile類型的變量baseCount記錄元素的個數,當插入新數據或則刪除數據時,會通過addCount()方法更新baseCount

                        初始化時counterCells為空,在並發量很高時,如果存在兩個線程同時執行CAS修改baseCount值,則失敗的線程會繼續執行方法體中的邏輯,使用CounterCell記錄元素個數的變化;

                        如果CounterCell數組counterCells為空,調用fullAddCount()方法進行初始化,並插入對應的記錄數,通過CAS設置cellsBusy字段,只有設置成功的線程才能初始化CounterCell數組

                        如果通過CAS設置cellsBusy字段失敗的話,則繼續嘗試通過CAS修改baseCount字段,如果修改baseCount字段成功的話,就退出循環,否則繼續循環插入CounterCell對象;

                        統計長度的方法: 在1.8中的size實現比1.7簡單多,因為元素個數保存baseCount中,部分元素的變化個數保存在CounterCell數組中

2. HashMap:

    (JKD1.7) 數組 + 鏈表 ;

                        hash函數,通過hash函數計算出下標;

                        put和get方法,先進行hash 得到位置,然后有沒有值,如果有值,再通過equal方法比較 鏈表上的所有值;如果沒有相同的,新值放在鏈表的最上面。

                        HashMap最多只允許一條記錄的鍵為null,允許多條記錄的值為null。HashMap非線程安全,即任一時刻可以有多個線程同時寫HashMap,可能會導致數據的不一致。

                        為什么線程不安全?通過Entry內部的next變量可以知道使用的是鏈表,這時候我們可以知道,如果多個線程,在某一時刻同時操作HashMap並執行put操作,而有超過兩個key的hash值相同,如圖中a1、a2,

                        這個時候需要解決碰撞沖突,而解決沖突的辦法上面已經說過,對於鏈表的結構在這里不再贅述,暫且不討論是從鏈表頭部插入還是從尾部初入,這個時候兩個線程如果恰好都取到了對應位置的頭結點e1,

                        而最終的結果可想而知,a1、a2兩個數據中勢必會有一個會丟失。

                        當多個線程同時檢測到總數量超過門限值的時候就會同時調用resize操作,各自生成新的數組並rehash后賦給該map底層的數組table,結果最終只有最后一個線程生成的新數組被賦給table變量,其他線程的

                        均會丟失。而且當某些線程已經完成賦值而其他線程剛開始的時候,就會用已經被賦值的table作為原始數組,這樣也會有問題。

   (JDK1.8)  負載因子和Hash算法設計的再合理,也免不了會出現拉鏈過長的情況,一旦出現拉鏈過長,則會嚴重影響HashMap的性能。於是,在JDK1.8版本中,對數據結構做了進一步的優化,引入了紅黑樹。而當鏈表

                        長度太長(默認超過8)時,鏈表就轉換為紅黑樹,利用紅黑樹快速增刪改查的特點提高HashMap的性能,其中會用到紅黑樹的插入、刪除、查找等算法。

                        鏈表長度 到8以后,轉換為紅黑樹。長度減小到6以后,重新轉換為鏈表。

                        transient Node<K,V>[] table;   // 1.8 以后的數組

 

     HashMap里面的紅黑樹是 TreeNode , 

     ConcurrentHashMap 紅黑樹對象是TreeBin,TreeBin里面有  TreeNode<K,V> root; 成員變量,還有一個 lockState ,估計是為了方便加鎖。

    

3. LinkedHashMap:LinkedHashMap是HashMap的一個子類,保存了記錄的插入順序,在用Iterator遍歷LinkedHashMap時,先得到的記錄肯定是先插入的,也可以在構造時帶參數,按照訪問次序排序。

                              HashMap和雙向鏈表合二為一即是LinkedHashMap。所謂LinkedHashMap,其落腳點在HashMap因此更准確地說,它是一個將所有Entry節點鏈入一個雙向鏈表雙向鏈表的HashMap。

                              在LinkedHashMapMap中,所有put進來的Entry都保存在如下面第一個圖所示的哈希表中,但由於它又額外定義了一個以head為頭結點的雙向鏈表(如下面第二個圖所示),因此對於每次put進來Entry,

                              除了將其保存到哈希表中對應的位置上之外,還會將其插入到雙向鏈表的尾部。

                              

 

1. 如果哈希桶數組很大,即使較差的Hash算法也會比較分散,如果哈希桶數組數組很小,即使好的Hash算法也會出現較多碰撞,所以就需要在空間成本和時間成本之間權衡,其實就是在根據實際情況確定哈希桶數組的大小,

    並在此基礎上設計好的hash算法減少Hash碰撞。那么通過什么方式來控制map使得Hash碰撞的概率又小,哈希桶數組(Node<k,v>[] table)占用空間又少呢?答案就是好的Hash算法和擴容機制。

    當然Hash算法計算結果越分散均勻,Hash碰撞的概率就越小,map的存取效率就會越高。

2. Node<k,v>[] table的初始化長度length(默認值是16),Load factor為負載因子(默認值是0.75),threshold是HashMap所能容納的最大數據量的Node(鍵值對)個數。threshold = length * Load factor。

    也就是說,在數組定義好長度之后,負載因子越大,所能容納的鍵值對個數越多。

3. 結合負載因子的定義公式可知,threshold就是在此Load factor和length(數組長度)對應下允許的最大元素數目,超過這個數目就重新resize(擴容),擴容后的HashMap容量是之前容量的兩倍。默認的負載因子0.75是

    對空間和時間效率的一個平衡選擇,建議大家不要修改除非在時間和空間比較特殊的情況下如果內存空間很多而又對時間效率要求很高,可以降低負載因子Load factor的值;相反,如果內存空間緊張而對時間效率

    要求不高,可以增加負載因子loadFactor的值,這個值可以大於1。

4. 在HashMap中,哈希桶數組table的長度length大小必須為2的n次方(一定是合數),這是一種非常規的設計,常規的設計是把桶的大小設計為素數。相對來說素數導致沖突的概率要小於合數,具體證明可以

    參考http://blog.csdn.net/liuqiyao_01/article/details/14475159,Hashtable初始化桶大小為11,就是桶大小設計為素數的應用(Hashtable擴容后不能保證還是素數)。HashMap采用這種非常規設計,主要是為了

     在取模和擴容時做優化,同時為了減少沖突,HashMap定位哈希桶索引位置時,也加入了高位參與運算的過程。

5. 這里存在一個問題,即使本文不再對紅黑樹展開討論,想了解更多紅黑樹數據結構的工作原理可以參考http://blog.csdn.net/v_july_v/article/details/6105630

6. 這里的Hash算法本質上就是三步:取key的hashCode值、高位運算、取模運算

7. 對於任意給定的對象,只要它的hashCode()返回值相同,那么程序調用方法一所計算得到的Hash碼值總是相同的。我們首先想到的就是把hash值對數組長度取模運算,這樣一來,元素的分布相對來說是比較均勻的。

    但是,模運算的消耗還是比較大的,在HashMap中是這樣做的:它通過h & (table.length -1)來得到該對象的保存位,而HashMap底層數組的長度總是2的n次方,這是HashMap在速度上的優化。當length總是2的n次方時,

    h& (length-1)運算等價於對length取模,也就是h%length,但是&比%具有更高的效率。

8. 在JDK1.8的實現中,優化了高位運算的算法,通過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質量來考慮的,這么做可以在數組table的length比較小的時候,

    也能保證考慮到高低Bit都參與到Hash的計算中,同時不會有太大的開銷。

 9. 擴容(resize)就是重新計算容量,向HashMap對象里不停的添加元素,而HashMap對象內部的數組無法裝載更多的元素時,對象就需要擴大數組的長度,以便能裝入更多的元素。當然Java里的數組是無法自動擴容的,

      方法是使用一個新的數組代替已有的容量小的數組,就像我們用一個小桶裝水,如果想裝更多的水,就得換大水桶。

10. 紅黑樹和鏈表之間互相轉換的臨界值是6和8,中間有個差值7可以有效防止鏈表和樹頻繁轉換。假設一下,如果設計成鏈表個數超過8則鏈表轉換成樹結構,鏈表個數小於8則樹結構轉換成鏈表,如果一個HashMap不停的插入、刪除元素,

       鏈表個數在8左右徘徊,就會頻繁的發生樹轉鏈表、鏈表轉樹,效率會很低。

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

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

     這個設計確實非常的巧妙,既省去了重新計算hash值的時間,而且同時,由於新增的1bit是0還是1可以認為是隨機的,因此resize的過程,均勻的把之前的沖突的節點分散到新的bucket了。這一塊就是JDK1.8新增的優化點。

12. 有一點注意區別,JDK1.7中rehash的時候,舊鏈表遷移新鏈表的時候,如果在新表的數組索引位置相同,則鏈表元素會倒置,但是從上圖可以看出,JDK1.8不會倒置。有興趣的同學可以研究下JDK1.8的resize源碼,寫的很贊

13. HashMap中,如果key經過hash算法得出的數組索引位置全部不相同,即Hash算法非常好,那樣的話,getKey方法的時間復雜度就是O(1),如果Hash算法技術的結果碰撞非常多,假如Hash算極其差,所有的Hash算法結果

      得出的索引位置一樣,那樣所有的鍵值對都集中到一個桶中,或者在一個鏈表中,或者在一個紅黑樹中,時間復雜度分別為O(n)和O(lgn)。 鑒於JDK1.8做了多方面的優化,總體性能優於JDK1.7,下面我們從兩個方面用例子證明這一點。

 

小結

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

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

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

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

(5) 還沒升級JDK1.8的,現在開始升級吧。HashMap的性能提升僅僅是JDK1.8的冰山一角。

 

 

參見:https://tech.meituan.com/java-hashmap.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM