神奇的算法(一):HashMap(哈希映射)

1.什麼是HashMap?

HashMap,又稱哈希映射或散列圖。是一個用於儲存鍵—值對(key-value)的集合,每個鍵—值對又稱Entry,將這些Entry儲存在一個數組裏,這個數組就爲HashMap。
圖片轉自@程序員小灰
一般初始的HashMap爲空,如上圖所示。
而HashMap最主要有兩種方法:Put和Get方法。

2.HashMap的Put方法

Put方法就是將任意數據插入到HashMap中:
hashMap.put(“Mike”,0);
但是我們需要一個哈希函數來取得存入HashMap裏的位置Index:
index = hash(“Mike”);
一般來說初建HashMap的時候默認長度爲2的4次方冪,也就是hashMap.length=16。所以當越來越多的數據存進去的時候,難免會發生hash衝突,也就是index發生衝突:
圖片轉自@程序員小灰
這時候我們需要用上鍊表來解決這個問題,俗稱拉鍊哈希。在這裏,hashMap裏的每一個元素不僅僅只有一個Entry,也可以存在多個Entry。當後面儲存進來的數據的index與原有數據的index發生衝突時,使用“尾插法”創建鏈表,後來的Entry爲鏈表的“頭節點”,且通過next指向下一個Entry。
圖片轉自@程序員小灰
當然,也可以使用“頭插法”來解決hash衝突,只不過實際應用中一般後來存儲的數據使用頻率相比於先前的數據要高,所以“尾插法”創建鏈表使用比較廣泛。

3.HashMap的Get方法

Get方法就是通過Key的值來查找hashMap裏相對應的數據:
index = hash(“Mike”);
如果對應的index上有多個Entry,則順着鏈表的順序向下查找,直到找到相應的數據爲止。

4.如何確定HashMap的index?

首先,我們需要知道hashMap的初始默認長度爲16,可以通過手動或自動進行拓展hashMap的長度,並且Resize必須爲2的次方冪。
爲什麼Resize必須爲2的次方冪?
因爲確定index需要用位運算的方法:
index = hashCode & (hashMap.length-1);
此時默認情況下 hashMap.length-1 = 15,二進制爲1111,假設存入一個數據,計算其hashCode爲26437,二進制爲0110 0111 0011 0101,兩者做與運算得出的二進制爲0101,十進制爲7,則index = 7。
假如Resize不爲爲2的次方冪,則 hashMap.length-1 得出的二進制數可能爲0110,0001,0100等,此時如果和數據的hashCode值做與運算極易出現index衝突,即hashMap中某些元素爲null,某些元素有多個Entry,這大大降低了hashMap的使用性能。

5.關於計算HashCode的方法

在上面我們提到確定HashMap的index時需要用到hashCode,那什麼是hashCode?hashCode是根據數據對象的地址或者字符串或者數字算出來的int類型的數值。而對於數據對象中的每一個域a,我們需要通過其類型來計算對應的hashCode:
Ⅰ.如果爲Boolean類型,則:a ? 0 : 1
Ⅱ.如果爲char類型,則:( int ) a
Ⅲ.如果爲Long類型,則:( int )(a^(a >>> 32))
Ⅳ.如果爲Double類型,則先通過 double.doubleToLongBits( a ) 將其轉換成long類型,再進行進一步轉換。

具體計算方法可參照此處:
https://blog.csdn.net/lyx2007825/article/details/7846674

6.HashMap的容量擴展(Resize)

HashMap的容量是有限的。所以當數據不斷的插入就會逐漸產生越來越多的hash衝突,這會降低hashMap的性能,所以我們需要對hashMap進行Resize操作。
在進行Resize操作時,我們需要了解兩個決定hashMap實際長度的因素:
1.InitialCapacity(初始容量)
2.LoadFactor(負載因子)
其中InitialCapacity是定義給hashMap裏數組的容量,默認設置爲2的次方冪;LoadFactor表示一個散列表的空間的使用程度,LoadFactor越大則會降低空間開銷,但提高查找成本。反之,LoadFactor越小則會對空間造成浪費。
爲了在時間和空間中能夠尋找一種平衡,設置初始大小時應當使LoadFactor處於0.5~1之間,考慮預計的Entry在Map及其負載係數。所以一般默認LoadFactor = 0.75f。
於是hashMap.Size的計算公式爲:
HashMap.Size = InitialCapacity * LoadFactor
一旦hashMap需要Resize時,index就會出現改變,這時候不能盲目的往後擴容而不進行數據位置的更新,必須進行Rehash,也就是重新計算index並存入更新後的hashMap中。

7.HashMap的線程安全性

實際上,hashMap只有在單線程下是安全的。一旦處於高併發及多線程的情況下就會出現死鎖的情況。
爲什麼會出現死鎖的情況?先來看看Rehash的源代碼:

void transfer(Entry[] newTable,boolean rehash){
   int newCapacity = newTable.length;
   for (Entry<K,V>e:table){ 
      while (null != e){
          Entry<K,V> next = e.next;
          if(rehash){
            e.hash = null == e.key ? 0:hash(e.key);
          }
          int i = indexFor(e.hash,newCapacity);
          e.next = newTable[i];
          newTable[i] = e;
          e = next;
     }
  }
}

假設此時有線程A和線程B同時對一個含有鏈表hashMap進行rehash操作,這時候需要進行遍歷hashMap;當線程B執行到 Entry<K,V> next = e.next; 這一步代碼時會被掛起,此時線程A會一直執行rehash操作,完畢後將替換舊錶並存入內存之中。
這時候重點來了!
此時線程B解掛,會繼續執行rehash操作(按照舊錶的結構進行rehash),但是此時線程A所構建的hashMap已經替換舊錶。假若新的hashMap裏元素中Entry只有一個則沒有影響,關鍵是hashMap中存在鏈表的時候,舊錶中鏈表爲:Entry2->Entry1,線程A rehash之後鏈表結構爲:Entry1->Entry2,但此時線程B按照舊錶的結構進行rehash,當執行到 newTable[i] = e; e = next; 這兩步時:
newTable[i] = Entry2
e = Entry1
Entry2.next = Entry1
Entry1.next = Entry2

出現了環形鏈表,也就是死鎖

8.HashMap在高併發下出現死鎖的解決辦法

Ⅰ.改用hashTable方法
在hashTable方法中,使用了synchronized關鍵字,這是JAVA中一個內置鎖的加鎖機制,此時在多線程的情況下,每一個線程在進行rehash操作的時候機會將整個hashMap進行加鎖,直到該線程操作執行完畢才能解鎖。
但是,如果整個hashMap很大並且在併發線程數量多的時候使用這個方法,就會大大降低性能。所以比較適合線程數量不多的情況下。

Ⅱ.改用ConcurrentHashMap方法
在用這個方法之前,需要了解一個概念:segment
實際上Segment是一個小型的HashMap ,其中也包含一個HashEntry數組。換而言之,ConcurrentHashMap是一個雙層哈希表,ConcurrentHashMap裏每一個Entry下也包含一個hashMap。之所以這樣設計,優勢主要在於: **讓每個segment的讀寫是高度自治的,segment之間互不影響。這稱之爲“鎖分段技術”;**相比於hashTable方法來說,並不需要將整個hashMap進行加鎖,只需要將每個segment加鎖,讓多線程在進行Put操作的時候能夠將鎖的粒度保持地儘量地小,提高頻率。

注:該文章圖片均來源於公衆號——程序員小灰(非本人公衆號),感興趣的可以關注一下!

以上是本人對HashMap的理解,如有疏漏與不足的地方,懇求大神指點。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章