面試HashMap之追命5連問

 

 

一、HashMap底層實現數據結構?

總的來講,HashMap就是數組+鏈表的組合實現,每一個數組元素存儲一個鏈表的頭結點,本質上來講是哈希表「拉鍊法」的實現。java

HashMap的鏈表元素對應的是一個靜態內部類Entry,Entry主要包含key,value,next三個元素面試

在Jdk1.8中HashMap的實現方式作了一些改變,可是基本思想仍是沒有變得,只是在一些地方作了優化,下面來看一下這些改變的地方,數據結構的存儲由數組+鏈表的方式,變化爲數組+鏈表+紅黑樹的存儲方式,在性能上進一步獲得提高。算法

二、 如何解決Hash衝突? put方法原理?

HashMap 採用一種所謂的「Hash 算法」來決定每一個元素的存儲位置。當程序執行 map.put(String,Obect)方法 時,系統將調用String的 hashCode() 方法獲得其 hashCode 值——每一個 Java 對象都有 hashCode() 方法,均可經過該方法得到它的 hashCode 值。獲得這個對象的 hashCode 值以後,系統會根據該 hashCode 值來決定該元素的存儲位置數組

put方法分析:安全

public V put(K key, V value) {
    //調用putVal()方法完成
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //判斷table是否初始化,不然初始化操做
    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);
                    //鏈表長度8,將鏈表轉化爲紅黑樹存儲
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //key存在,直接覆蓋
                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();
    //空操做
    afterNodeInsertion(evict);
    return null;
}

  

下面將這個過程總結一下:數據結構

一、計算key的hash值,算出元素在底層數組中的下標位置。多線程

二、經過下標位置定位到底層數組裏的元素(也有多是鏈表也有多是樹)。併發

三、取到元素,判斷放入元素的key是否==或equals當前位置的key,成立則替換value值,返回舊值。app

四、若是是樹,循環樹中的節點,判斷放入元素的key是否==或equals節點的key,成立則替換樹裏的value,並返回舊值,不成立就添加到樹裏。函數

五、不然就順着元素的鏈表結構循環節點,判斷放入元素的key是否==或equals節點的key,成立則替換鏈表裏value,並返回舊值,找不到就添加到鏈表的最後。

精簡一下,判斷放入HashMap中的元素要不要替換當前節點的元素,key知足如下兩個條件便可替換:

一、hash值相等。

二、==或equals的結果爲true。

 

三、 爲何String, Interger這樣的類適合做爲鍵?

String, Interger這樣的類做爲HashMap的鍵是再適合不過了,並且String最爲經常使用。 
由於String對象是不可變的,並且已經重寫了equals()和hashCode()方法了。


  1.不可變性是必要的,由於爲了要計算hashCode(),就要防止鍵值改變,若是鍵值在放入時和獲取時返回不一樣的hashcode的話,那麼就不能從HashMap中找到你想要的對象。不可變性還有其餘的優勢如線程安全。

2.由於獲取對象的時候要用到equals()和hashCode()方法,那麼鍵對象正確的重寫這兩個方法是很是重要的。若是兩個不相等的對象返回不一樣的hashcode的話,那麼碰撞的概率就會小些,這樣就能提升HashMap的性能。
 

四、HashMap與HashTable的區別?

Hashtable能夠看作是線程安全版的HashMap,二者幾乎「等價」(固然仍是有不少不一樣)。Hashtable幾乎在每一個方法上都加上synchronized(同步鎖),實現線程安全。

區別
  1.HashMap繼承於AbstractMap,而Hashtable繼承於Dictionary; 
  2.線程安全不一樣。Hashtable的幾乎全部函數都是同步的,即它是線程安全的,支持多線程。而HashMap的函數則是非同步的,它不是線程安全的。若要在多線程中使用HashMap,須要咱們額外的進行同步處理; 
  3.null值。HashMap的key、value均可覺得null。Hashtable的key、value都不能夠爲null; 
  4.迭代器(Iterator)。HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。因此當有其它線程改變了HashMap的結構(增長或者移除元素),將會拋出ConcurrentModificationException。 
  5.容量的初始值和增長方式都不同:HashMap默認的容量大小是16;增長容量時,每次將容量變爲「原始容量x2」。Hashtable默認的容量大小是11;增長容量時,每次將容量變爲「原始容量x2 + 1」; 
  6.添加key-value時的hash值算法不一樣:HashMap添加元素時,是使用自定義的哈希算法。Hashtable沒有自定義哈希算法,而直接採用的key的hashCode()。 
  7.速度。因爲Hashtable是線程安全的也是synchronized,因此在單線程環境下它比HashMap要慢。若是你不須要同步,只須要單一線程,那麼使用HashMap性能要好過Hashtable。

可否讓HashMap同步? 
HashMap能夠經過下面的語句進行同步:Map m = Collections.synchronizeMap(hashMap);

 

五、CurrentHashMap是如何實現併發的?

HashTable容器在競爭激烈的併發環境下表現出效率低下的緣由,是由於全部訪問HashTable的線程都必須競爭同一把鎖。

那假如容器裏有多把鎖,每一把鎖用於鎖容器其中一部分數據,那麼當多線程訪問容器裏不一樣數據段的數據時,線程間就不會存在鎖競爭,從而能夠有效的提升併發訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術。

首先將數據分紅一段一段的存儲,而後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據的時候,其餘段的數據也能被其餘線程訪問。有些方法須要跨段,好比size()和containsValue(),它們可能須要鎖定整個表而而不只僅是某個段,這須要按順序鎖定全部段,操做完畢後,又按順序釋放全部段的鎖。

這裏「按順序」是很重要的,不然極有可能出現死鎖,在ConcurrentHashMap內部,段數組是final的,而且其成員變量實際上也是final的,可是,僅僅是將數組聲明爲final的並不保證數組成員也是final的,這須要實現上的保證。這能夠確保不會出現死鎖,由於得到鎖的順序是固定的。

ConcurrentHashMap是由Segment數組結構和HashEntry數組結構組成。

Segment是一種可重入鎖ReentrantLock,在ConcurrentHashMap裏扮演鎖的角色,HashEntry則用於存儲鍵值對數據。一個ConcurrentHashMap裏包含一個Segment數組,Segment的結構和HashMap相似,是一種數組和鏈表結構, 一個Segment裏包含一個HashEntry數組,每一個HashEntry是一個鏈表結構的元素, 每一個Segment守護者一個HashEntry數組裏的元素,當對HashEntry數組的數據進行修改時,必須首先得到它對應的Segment鎖。

JDK1.8中的實現

ConcurrentHashMap取消了segment分段鎖,而採用CAS和synchronized來保證併發安全。數據結構跟HashMap1.8的結構同樣,數組+鏈表/紅黑二叉樹。
synchronized只鎖定當前鏈表或紅黑二叉樹的首節點,這樣只要hash不衝突,就不會產生併發,效率又提高N倍。

JDK1.8的ConcurrentHashMap的結構圖以下:

TreeBin: 紅黑二叉樹節點Node: 鏈表節點

相關文章
相關標籤/搜索