Java集合之HashMap知多少

一.引言    java

 HashMap應該算是Java後端工程師面試的必問題,由於其中的知識點太多,很適合用來考察面試者的Java基礎。面試

 HashMap做爲使用頻率最高的用於映射(鍵值對)處理的數據類型。隨着JDK(Java Developmet Kit)版本的更新,JDK1.8對HashMap底層的實現進行了優化, 例如引入紅黑樹的數據結構和擴容的優化等。 結合JDK1.7和JDK1.8的區別讓咱們一塊兒來探討一下吧!算法

二.簡介數據庫

        Hash法的概念後端

         散列法(Hashing)是一種將字符組成的字符串轉換爲固定長度(通常是更短長度)的數值或索引值的方法,稱爲散列法,也叫哈希法。因爲經過更短的哈希值比用原始值進行數據庫搜索更快,這種方法通常用來在數據庫中創建索引並進行搜索,同時還用在各類解密算法中。數組

        HashMap 和 HashTable安全

        Hashtable 是早期Java類庫提供的一個哈希表實現, 不少映射的經常使用功能與HashMap相似,不一樣的是它承自Dictionary類,而且是線程安全的,任一時間只有一個線程能寫Hashtable ,不支持 null 鍵和null值,因爲同步致使的性能開銷,因此已經不多被推薦使用。數據結構

        HashMap與 HashTable主要區別在於 HashMap 不是同步的,支持 null 鍵和null值等。一般狀況下,HashMap 進行 put 或者 get 操做,能夠達到常數時間的性能,因此它是絕大部分利用鍵值對存取場景的首選。HashMap非線程安全,即任一時刻能夠有多個線程同時寫HashMap,可能會致使數據的不一致。若是須要知足線程安全,能夠用 Collections的synchronizedMap方法使HashMap具備線程安全的能力,或者使用ConcurrentHashMap。多線程

三.HashMap的實現原理併發

     HashMap由數組+鏈表組成的,數組是HashMap的主體,鏈表則是主要爲了解決哈希衝突而存在的,若是定位到的數組位置不含鏈表(當前entry的next指向null),那麼查找,添加等操做很快,僅需一次尋址便可;若是定位到的數組包含鏈表,對於添加操做,其時間複雜度爲O(n),首先遍歷鏈表,存在即覆蓋,不然新增;對於查找操做來說,仍需遍歷鏈表,而後經過key對象的equals方法逐一比對查找。因此,性能考慮,HashMap中的鏈表出現越少,性能纔會越好。

    

 哈希衝突

咱們都知道判斷對象是否相等的時候可使用 hash值這種方式來,同時咱們還知道這種方式是存在缺點的,有可能兩個對象的內容不正確,可是兩個對象的hash值卻同樣,這是存在的.

   那麼, 若是兩個不一樣的元素,經過哈希函數得出的實際存儲地址相同怎麼辦 也就是說,當咱們對某個元素進行哈希運算,獲得一個存儲地址,而後要進行插入的時候,發現已經被其餘元素佔用了,其實這就是所謂的哈希衝突,也叫哈希碰撞

    數組是一塊連續的固定長度的內存空間,再好的哈希函數也不能保證獲得的存儲地址絕對不發生衝突。那麼哈希衝突如何解決呢?

  咱們能夠經過如下幾種方式解決 : 

針對哈希表直接定址可能存在hash衝突,舉一個簡單的例子,例如:
    第一個鍵值對A進來,經過計算其key的hash獲得的index=0。記作:Entry[0] = A。
    第二個鍵值對B,經過計算其index也等於0, HashMap會將B.next =A,Entry[0] =B,
    第三個鍵值對C,經過計算其index也等於0,那麼C.next = B,Entry[0] = C;
這樣咱們發現index=0的地方事實上存取了A,B,C三個鍵值對,它們經過next這個屬性連接在一塊兒。 對於不一樣的元素,可能計算出了相同的函數值,這樣就產生了hash 衝突,那要解決衝突,又有哪些方法呢?具體以下:

a. 鏈地址法:將哈希表的每一個單元做爲鏈表的頭結點,全部哈希地址爲 i 的元素構成一個同義詞鏈表。即發生衝突時就把該關鍵字鏈在以該單元爲頭結點的鏈表的尾部。

b. 開放定址法:即發生衝突時,去尋找下一個空的哈希地址。只要哈希表足夠大,總能找到空的哈希地址。

c. 再哈希法:即發生衝突時,由其餘的函數再計算一次哈希值。

d. 創建公共溢出區:將哈希表分爲基本表和溢出表,發生衝突時,將衝突的元素放入溢出表。

而咱們的HashMap採用的是鏈地址法,即上面提到 數組 + 鏈表的方式. 當兩個對象的哈希值相同時,它們的哈希桶位置相同,碰撞就會發生。此時,能夠將 put 進來的 K- V 對象插入到鏈表的尾部。對於儲存在同一個哈希桶位置的鏈表對象,可經過鍵對象的equals()方法用來找到鍵值對。

四.HashMap的內部數據結構

        HashMap內部維護的數據結構是數組+鏈表,每一個鍵值對都存儲在HashMap的靜態內部類Entry中,結構如圖 :

五 . HashMap插入數據原理圖

  1. 判斷數組是否爲空,爲空進行初始化;
  2. 不爲空,計算 k 的 hash 值,經過 (n-1) & hash 計算應當存放在數組中的下標 index;
  3. 查看 table[index] 是否存在數據,沒有數據就構造一個Node節點存放在 table[index] 中;
  4. 存在數據,說明發生了hash衝突(存在二個節點key的hash值同樣), 繼續判斷key是否相等,相等,用新的value替換原數據(onlyIfAbsent爲false);
  5. 若是不相等,判斷當前節點類型是否是樹型節點,若是是樹型節點,創造樹型節點插入紅黑樹中;(若是當前節點是樹型節點證實當前已是紅黑樹了)
  6. 若是不是樹型節點,建立普通Node加入鏈表中;判斷鏈表長度是否大於 8而且數組長度大於64, 大於的話鏈表轉換爲紅黑樹;
  7. 插入完成以後判斷當前節點數是否大於閾值,若是大於開始擴容爲原數組的二倍。

六.HashMap 是否線程安全?如何解決線程不安全的問題

    HashMap是否線程安全?    

        不是,在多線程環境下,1.7 會產生死循環、數據丟失、數據覆蓋的問題,1.8 中會有數據覆蓋的問題,以1.8爲例,當A線程判斷index位置爲空後正好掛起,B線程開始往index位置的寫入節點數據,這時A線程恢復現場,執行賦值操做,就把A線程的數據給覆蓋了;還有++size這個地方也會形成多線程同時擴容等問題。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
  Node<K,V>[] tab; Node<K,V> p; int n, i;
  if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
  if ((p = tab[i = (n - 1) & hash]) == null)  //多線程執行到這裏
    tab[i] = newNode(hash, key, value, null);
  else {
    Node<K,V> e; K k;
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
      e = p;
    else if (p instanceof TreeNode) // 這裏很重要
      e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    else {
      for (int binCount = 0; ; ++binCount) {
        if ((e = p.next) == null) {
          p.next = newNode(hash, key, value, null);
          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            treeifyBin(tab, hash);
          break;
        }
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
          break;
        p = e;
      }
    }
    if (e != null) { // existing mapping for key
      V oldValue = e.value;
      if (!onlyIfAbsent || oldValue == null)
        e.value = value;
      afterNodeAccess(e);
      return oldValue;
    }
  }
  ++modCount;
  if (++size > threshold) // 多個線程走到這,可能重複resize()
    resize();
  afterNodeInsertion(evict);
  return null;
}

如何解決線程不安全的問題?

Java中有HashTable、Collections.synchronizedMap、以及ConcurrentHashMap能夠實現線程安全的Map。

HashTable是直接在操做方法上加synchronized關鍵字,鎖住整個數組,粒度比較大;

Collections.synchronizedMap是使用Collections集合工具的內部類,經過傳入Map封裝出一個SynchronizedMap對象,內部定義了一個對象鎖,方法內經過對象鎖實現;

ConcurrentHashMap使用分段鎖,下降了鎖粒度,讓併發度大大提升。

 

七.HashMap 在 JDk 1.8 和 JDK 1.7 中有什麼區別?

 1.發生hash衝突時 

    JDK 1.7  :

             發生hash衝突時,新元素插入到鏈表頭中,即新元素老是添加到數組中,就元素移動到鏈表中。

    JDK 1.8 :

             發生hash衝突後,會優先判斷該節點的數據結構式是紅黑樹仍是鏈表,若是是紅黑樹,則在紅黑樹中插入數據;若是是鏈表,則將數據插入到鏈表的尾部並判斷鏈表長度是否大於8,若是大於8要轉成紅黑樹。

2.新增節點時

        JDK 1.7 :

              使用了 頭插法

        JDK 18. :

               使用了 尾插法
3.擴容時

     JDK 1.7 :

          在擴容resize()過程當中,採用單鏈表的頭插入方式,在將舊數組上的數據 轉移到 新數組上時,轉移操做 = 按舊鏈表的正序遍歷鏈表、在新鏈表的頭部依次插入,即在轉移數據、擴容後,容易出現鏈表逆序的狀況 。 多線程下resize()容易出現死循環。此時若(多線程)併發執行 put()操做,一旦出現擴容狀況,則 容易出現 環形鏈表,從而在獲取數據、遍歷鏈表時 造成死循環(Infinite Loop),即 死鎖的狀態 。

    JDK 1.8 :

        因爲 JDK 1.8 轉移數據操做 = 按舊鏈表的正序遍歷鏈表、在新鏈表的尾部依次插入,因此不會出現鏈表 逆序、倒置的狀況,故不容易出現環形鏈表的狀況 ,但jdk1.8還是線程不安全的,由於沒有加同步鎖保護。

建議 : 

    1.使用的時候設置初始化值,避免屢次擴容的性能消耗

    2.使用自定義對象做爲key時,須要重寫hashCode()和equals()方法

    3.多線程環境儘可能使用ConcurrentHashMap來代替HashMap,HashTable也能夠,可是用的少

這就是小喵今天的分享了

知識有點亂,還請小夥伴們到時候縷縷思路哦!

(^_^)~喵~!!

相關文章
相關標籤/搜索