JAVA面試題整理 || HashMap具體分析

1. 底層結構

//默認初始容量爲16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
//默認負載因子爲0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//Hash數組(在resize()中初始化)
transient Node<K,V>[] table;
//元素個數
transient int size;
//容量閾值(元素個數超過該值會自動擴容)  
int threshold;


// 底層結構時Node組成的數組,下面時Node的實現
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
    
    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);//^表示相同返回0,不同返回1
        //Objects.hashCode(o)————>return o != null ? o.hashCode() : 0;
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            //Objects.equals(1,b)————> return (a == b) || (a != null && a.equals(b));
            if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

2. 擴容

主要邏輯是resize方法,如下

/*擴容*/
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 容量閾值(元素個數超過該值會自動擴容)
    int oldThr = threshold;
    int newCap, newThr = 0;
    //1、若舊數組長度oldCap>0 說明hash數組table已被初始化
    if (oldCap > 0) {
        // 如果舊的數組長度大於等於位運算結果(1 << 30),即達到最大容量無法擴容,返回舊的長度
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }//按當前table數組長度的2倍進行擴容,容量閾值也變爲原來的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; 
    }//2、若數組未被初始化,而threshold>0說明調用了HashMap(initialCapacity)和HashMap(initialCapacity, loadFactor)構造器
    else if (oldThr > 0)
        newCap = oldThr;//新容量設爲數組閾值
    else { //3、若table數組未被初始化,且threshold爲0說明調用HashMap()構造方法             
        newCap = DEFAULT_INITIAL_CAPACITY;//默認初始容量爲16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//默認初始擴容閾值16*0.75
    }
    
    //若計算過程中,閾值溢出歸零,則按閾值公式重新計算
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    //創建新的hash數組,hash數組的初始化也是在這裏完成的
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    //如果舊的hash數組不爲空,則遍歷舊數組並映射到新的hash數組
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;//GC
                if (e.next == null)//如果只鏈接一個節點,重新計算並放入新數組
                    newTab[e.hash & (newCap - 1)] = e;
                //若是紅黑樹,則需要進行拆分    
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { 
                    //rehash————>重新映射到新數組
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        /*注意這裏使用的是:e.hash & oldCap,若爲0則索引位置不變,不爲0則新索引=原索引+舊數組長度*/
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

1.HashMap

我們知道HashMap是線程不安全的,在多線程環境下,使用Hashmap進行put操作會引起死循環,導致CPU利用率接近100%,所以**在併發情況下不能使用HashMap。

2.HashTable

HashTable和HashMap的實現原理幾乎一樣,差別無非是

  • HashTable不允許key和value爲null
  • HashTable是線程安全的

但是HashTable線程安全的策略實現代價卻太大了,簡單粗暴,get/put所有相關操作都是synchronized的,這相當於給整個哈希表加了一把大鎖。

多線程訪問時候,只要有一個線程訪問或操作該對象,那其他線程只能阻塞,相當於將所有的操作串行化,在競爭激烈的併發場景中性能就會非常差。

3.ConcurrentHashMap

主要就是爲了應對hashmap在併發環境下不安全而誕生的,ConcurrentHashMap的設計與實現非常精巧,大量的利用了volatile,final,CAS等lock-free技術來減少鎖競爭對於性能的影響。

我們都知道Map一般都是數組+鏈表結構(JDK1.8該爲數組+紅黑樹)。

file

ConcurrentHashMap避免了對全局加鎖改成了局部加鎖操作,這樣就極大地提高了併發環境下的操作速度,由於ConcurrentHashMap在JDK1.7和1.8中的實現非常不同,接下來我們談談JDK在1.7和1.8中的區別。

4.JDK1.7版本的CurrentHashMap的實現原理

在JDK1.7中ConcurrentHashMap採用了數組+Segment+分段鎖的方式實現。

1.Segment(分段鎖)

ConcurrentHashMap中的分段鎖稱爲Segment,它即類似於HashMap的結構,即內部擁有一個Entry數組,數組中的每個元素又是一個鏈表,同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。

2.內部結構

ConcurrentHashMap使用分段鎖技術,將數據分成一段一段的存儲,然後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據的時候,其他段的數據也能被其他線程訪問,能夠實現真正的併發訪問。如下圖是ConcurrentHashMap的內部結構圖:

file

從上面的結構我們可以瞭解到,ConcurrentHashMap定位一個元素的過程需要進行兩次Hash操作。

第一次Hash定位到Segment,第二次Hash定位到元素所在的鏈表的頭部。

3.該結構的優劣勢

壞處

這一種結構的帶來的副作用是Hash的過程要比普通的HashMap要長

好處

寫操作的時候可以只對元素所在的Segment進行加鎖即可,不會影響到其他的Segment,這樣,在最理想的情況下,ConcurrentHashMap可以最高同時支持Segment數量大小的寫操作(剛好這些寫操作都非常平均地分佈在所有的Segment上)。

所以,通過這一種結構,ConcurrentHashMap的併發能力可以大大的提高。

5. JDK1.8版本的CurrentHashMap的實現原理

JDK8中ConcurrentHashMap參考了JDK8 HashMap的實現,採用了數組+鏈表+紅黑樹的實現方式來設計,內部大量採用CAS操作,這裏我簡要介紹下CAS

CAS是compare and swap的縮寫,即我們所說的比較交換。cas是一種基於鎖的操作,而且是樂觀鎖。在java中鎖分爲樂觀鎖和悲觀鎖。悲觀鎖是將資源鎖住,等一個之前獲得鎖的線程釋放鎖之後,下一個線程纔可以訪問。而樂觀鎖採取了一種寬泛的態度,通過某種方式不加鎖來處理資源,比如通過給記錄加version來獲取數據,性能較悲觀鎖有很大的提高。

CAS 操作包含三個操作數 —— 內存位置(V)、預期原值(A)和新值(B)。如果內存地址裏面的值和A的值是一樣的,那麼就將內存裏面的值更新成B。CAS是通過無限循環來獲取數據的,若果在第一輪循環中,a線程獲取地址裏面的值被b線程修改了,那麼a線程需要自旋,到下次循環纔有可能機會執行。

JDK8中徹底放棄了Segment轉而採用的是Node,其設計思想也不再是JDK1.7中的分段鎖思想.

Node:保存key,value及key的hash值的數據結構。其中value和next都用volatile修飾,保證併發的可見性。

class Node<K,V implements Map.Entry<K,V {**
	final int hash;
	final K key;
	volatile V val;
	volatile Node<K,V next;
	//... 省略部分代碼
} 

Java8 ConcurrentHashMap結構基本上和Java8的HashMap一樣,不過保證線程安全性。

在JDK8中ConcurrentHashMap的結構,由於引入了紅黑樹,使得ConcurrentHashMap的實現非常複雜,我們都知道,紅黑樹是一種性能非常好的二叉查找樹,其查找性能爲O(logN),但是其實現過程也非常複雜,而且可讀性也非常差,DougLea的思維能力確實不是一般人能比的,早期完全採用鏈表結構時Map的查找時間複雜度爲O(N),JDK8中ConcurrentHashMap在鏈表的長度大於8的時候會將鏈表轉換成紅黑樹進一步提高其查找性能。

file

總結

其實可以看出JDK1.8版本的ConcurrentHashMap的數據結構已經接近HashMap,相對而言,ConcurrentHashMap只是增加了同步的操作來控制併發,從JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+紅黑樹。

1.數據結構:**取消了Segment分段鎖的數據結構,取而代之的是數組+鏈表+紅黑樹的結構。

2.保證線程安全機制:**JDK1.7採用segment的分段鎖機制實現線程安全,其中segment繼承自ReentrantLock。JDK1.8採用CAS+Synchronized保證線程安全。

3.鎖的粒度:原來是對需要進行數據操作的Segment加鎖,現調整爲對每個數組元素加鎖(Node)。

4.鏈表轉化爲紅黑樹:**定位結點的hash算法簡化會帶來弊端,Hash衝突加劇,因此在鏈表節點數量大於8時,會將鏈表轉化爲紅黑樹進行存儲。

5.查詢時間複雜度:**從原來的遍歷鏈表O(n),變成遍歷紅黑樹O(logN)。

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