1. 程式人生 > >【集合框架】HashMap原理及原始碼解讀

【集合框架】HashMap原理及原始碼解讀

本文加上個人理解,用自己的話表達集合框架及對HashMap細節的理解。

簡介

HashMap是一種利用鍵值對映儲存資料的資料結構,隨著jdk的發展,在jdk1.8中引入了紅黑樹的資料結構和擴容的優化。

Map類常用集合介紹

java.util.map類圖

HashMap實現自java.util.Map介面。此類集合常用的實現類有四種:HashMap,LinkedHashMap,HashTable,TreeMap。

  1. HashMap類特點:它根據鍵key的hashcode來定位數值的儲存位置(若是物件,不是直接利用物件的hashcode()方法,而是重新計算)。但是遍歷的順序是不確定的,是一種高效查詢鍵對應位置的資料集合,相對HashTable更加高效。它允許null作為鍵值,鍵中只存在一個null值,但是值可以重複可以存在多個null值。HashMap不是執行緒安全的,在多執行緒程式設計中建議使用ConcurrentHashMap提供執行緒安全的map類。
  2. HashTable類特點:繼承自Dictionry類。是一個遺留類。提供與HashMap相同的鍵值對映功能,但是它是執行緒安全的,併發效能不如ConcurrentHashMap,僅僅是內部使用synchronized 關鍵字修飾put()等方法,而ConcurrentHashMap引入分段鎖機制。同時HashTable不允許null作為鍵值。相比HashMap效率較低,初始容量和擴容方式分別為11與2*n+1;在方法上也存在於HashMap不同之處。
  3. LinkedHashMap特點:HashMap的一個子類,插入時儲存了插入的順序,當使用Iterator遍歷的時候,輸出的順序為插入的順序,也可以在初始化的傳入比較器用於排序。
  4. TreeMap特點:TreeMap實現了SortedMap介面,可以根據存入的資料的key對其進行排序,底層是用紅黑樹實現的,排序順序是自然排序(字典排序)。也可以傳入比較器new Comparator

HashMap的主要知識點:

  1.  HashMap是一種結合了陣列和連結串列優點的資料結構,利用鏈地址法實現雜湊表。
  2. 它是一種鍵值對對映關係的資料結構,執行緒不安全,允許null鍵值,遍歷時無序。
  3. HashMap底層是利用資料加連結串列實現的,陣列可以稱之為雜湊桶,每個雜湊桶中存放的是一張連結串列,連結串列中的每個節點對應是每一個元素(鍵值對)
  4. JDK1.8中,當連結串列中元素的個數大於8時,將轉換為紅黑樹來提升元素的查詢和增刪效率。
  5. 由於底層是陣列實現,當陣列容量不夠時設計擴容。HashMap的初始容量為16,每次擴容將增加為之前的2倍,所以其資料長度一定是2的n次方,這樣設計的目的是為了在計算key地址索引的時候可以利用位與代替取模,加快運算的效率。
  6. HashMap中的key索引不是直接利用的key的hashcode()方法,而是先獲取其雜湊值後,加入擾動函式(右移16位之後位與),這樣得到高位和低位的細節,減少雜湊衝突的可能性。
  7. 擴容時,對於HashMap來說是一個非常消耗效能的操作,每次擴容都是新建一個Node陣列,然後將老陣列中的元素重新計算雜湊地址存放入新的陣列中。
  8. HashMap針對擴容也做了優化,擴容後針對每個節點的索引下標可能會有變化,這個是根據計算出來的Hash值在對應高位上的位元值確定,若位元值為1,那麼索引就是oldlength+原索引。如果是1就是原索引不變。

原始碼解析

一、類宣告

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable

HashMap繼承自AbstractMap,實現了Map介面,Map介面定義了所有Map子類必須實現的方法。AbstractMap也實現了Map介面,並且提供了兩個實現Entry的內部類:SimpleEntry和SimpleImmutableEntry。

二、成員變數

//預設的初始容量,必須是2的冪。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量(必須是2的冪且小於2的30次方,傳入容量過大將被這個值替換)
static final int MAXIMUM_CAPACITY = 1 << 30;
//預設裝載因子,預設值為0.75,如果實際元素所佔容量佔分配容量的75%時就要擴容了。如果填充比很大,說明利用的空間很多,但是查詢的效率很低,因為連結串列的長度很大(當然最新版本使用了紅黑樹後會改進很多),HashMap本來是以空間換時間,所以填充比沒必要太大。但是填充比太小又會導致空間浪費。如果關注記憶體,填充比可以稍大,如果主要關注查詢效能,填充比可以稍小。
static final float _LOAD_FACTOR = 0.75f;

//一個桶的樹化閾值
//當桶中元素個數超過這個值時,需要使用紅黑樹節點替換連結串列節點
//這個值必須為 8,要不然頻繁轉換效率也不高
static final int TREEIFY_THRESHOLD = 8;

//一個樹的連結串列還原閾值
//當擴容時,桶中元素個數小於這個值,就會把樹形的桶元素 還原(切分)為連結串列結構
//這個值應該比上面那個小,至少為 6,避免頻繁轉換
static final int UNTREEIFY_THRESHOLD = 6;

//雜湊表的最小樹形化容量
//當雜湊表中的容量大於這個值時,表中的桶才能進行樹形化
//否則桶內元素太多時會擴容,而不是樹形化
//為了避免進行擴容、樹形化選擇的衝突,這個值不能小於 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;

//儲存資料的Entry陣列,長度是2的冪。
transient Entry[] table;
//
transient Set<Map.Entry<K,V>> entrySet;
//map中儲存的鍵值對的數量
transient int size;
//需要調整大小的極限值(容量*裝載因子)
int threshold;
//裝載因子
final float loadFactor;
//map結構被改變的次數
transient volatile int modCount;

內部類,連結串列節點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);
    }
    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;
            if (Objects.equals(key, e.getKey()) &&Objects.equals(value, e.getValue()))
                return true;
            }
        return false;
    }
}

三、構造方法

/**
*使用預設的容量及裝載因子構造一個空的HashMap
*/
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
}

/**
* 根據給定的初始容量和裝載因子建立一個空的HashMap
* 初始容量小於0或裝載因子小於等於0將報異常 
*/
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)//調整最大容量
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +loadFactor);
    this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
}

/**
*根據指定容量建立一個空的HashMap
*/
public HashMap(int initialCapacity) {
    //呼叫上面的構造方法,容量為指定的容量,裝載因子是預設值
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//通過傳入的map建立一個HashMap,容量為預設容量(16)和(map.zise()/DEFAULT_LOAD_FACTORY)+1的較大者,裝載因子為預設值
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

HashMap提供了四種構造方法:

(1)使用預設的容量及裝載因子構造一個空的HashMap;

(2)根據給定的初始容量和裝載因子建立一個空的HashMap;

(3)根據指定容量建立一個空的HashMap;

(4)通過傳入的map建立一個HashMap。

第三種構造方法會呼叫第二種構造方法,而第四種構造方法將會呼叫putMapEntries方法將元素新增到HashMap中去。

putMapEntries方法是一個final方法,不可以被修改,該方法實現了將另一個Map的所有元素加入表中,引數evict初始化時為false,其他情況為true

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    if (s > 0) {
        if (table == null) { 
        //根據m的元素數量和當前表的載入因子,計算出閾值
        float ft = ((float)s / loadFactor) + 1.0F;
        //修正閾值的邊界 不能超過MAXIMUM_CAPACITY
        int t = ((ft < (float)MAXIMUM_CAPACITY) ?(int)ft : MAXIMUM_CAPACITY);
        //如果新的閾值大於當前閾值
        if (t > threshold)
            //返回一個>=新的閾值的 滿足2的n次方的閾值
            threshold = tableSizeFor(t);
        }
        //如果當前元素表不是空的,但是 m的元素數量大於閾值,說明一定要擴容。
        else if (s > threshold)
            resize();
        //遍歷 m 依次將元素加入當前表中。
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);
        }
    }
}

其中,涉及到兩個操作,一個是計算新的閾值,另一個是擴容方法:

  1)如果新的閾值大於當前閾值,需要返回一個>=新的閾值的 滿足2的n次方的閾值,這涉及到了tableSizeFor:

static final int tableSizeFor(int cap) {
    //經過下面的 或 和位移 運算, n最終各位都是1。
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    //判斷n是否越界,返回 2的n次方作為 table(雜湊桶)的閾值
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

2)如果當前元素表不是空的,但是 m的元素數量大於閾值,說明一定要擴容。這涉及到了擴容方法resize,這是個人認為HashMap中最複雜的方法:

final Node<K,V>[] resize() {
    //oldTab 為當前表的雜湊桶
    Node<K,V>[] oldTab = table;
    //當前雜湊桶的容量 length
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //當前的閾值
    int oldThr = threshold;
    //初始化新的容量和閾值為0
    int newCap, newThr = 0;
    //如果當前容量大於0
    if (oldCap > 0) {
        //如果當前容量已經到達上限
        if (oldCap >= MAXIMUM_CAPACITY) {
            //則設定閾值是2的31次方-1
            threshold = Integer.MAX_VALUE;
            //同時返回當前的雜湊桶,不再擴容
            return oldTab;
        }//否則新的容量為舊的容量的兩倍。 
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
            oldCap >= DEFAULT_INITIAL_CAPACITY)
            //如果舊的容量大於等於預設初始容量16
            //那麼新的閾值也等於舊的閾值的兩倍
            newThr = oldThr << 1; // double threshold
    }
    //如果當前表是空的,但是有閾值。代表是初始化時指定了容量、閾值的情況
    else if (oldThr > 0) 
        newCap = oldThr;//那麼新表的容量就等於舊的閾值
    else {    
    //如果當前表是空的,而且也沒有閾值。代表是初始化時沒有任何容量/閾值引數的情況               
        newCap = DEFAULT_INITIAL_CAPACITY;//此時新表的容量為預設的容量 16
    //新的閾值為預設容量16 * 預設載入因子0.75f = 12
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        //如果新的閾值是0,對應的是  當前表是空的,但是有閾值的情況
        float ft = (float)newCap * loadFactor;//根據新表容量 和 載入因子 求出新的閾值
        //進行越界修復
        newThr = (newCap < MAXIMUM_CAPACITY && ft <(float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
    }
    //更新閾值 
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    //根據新的容量 構建新的雜湊桶
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    //更新雜湊桶引用
    table = newTab;
    //如果以前的雜湊桶中有元素
    //下面開始將當前雜湊桶中的所有節點轉移到新的雜湊桶中
    if (oldTab != null) {
        //遍歷老的雜湊桶
        for (int j = 0; j < oldCap; ++j) {
        //取出當前的節點 e
        Node<K,V> e;
        //如果當前桶中有元素,則將連結串列賦值給e
        if ((e = oldTab[j]) != null) {
            //將原雜湊桶置空以便GC
            oldTab[j] = null;
            //如果當前連結串列中就一個元素,(沒有發生雜湊碰撞)
            if (e.next == null)
            //直接將這個元素放置在新的雜湊桶裡。
            //注意這裡取下標 是用 雜湊值 與 桶的長度-1 。 由於桶的長度是2的n次方,這麼做其實是等於 一個模運算。但是效率更高
            newTab[e.hash & (newCap - 1)] = e;
            //如果發生過雜湊碰撞 ,而且是節點數超過8個,轉化成了紅黑樹
            else if (e instanceof TreeNode)
                 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
            //如果發生過雜湊碰撞,節點數小於8個。則要根據連結串列上每個節點的雜湊值,依次放入新雜湊桶對應下標位置。
            else {
                //因為擴容是容量翻倍,所以原連結串列上的每個節點,現在可能存放在原來的下標,即low位,或者擴容後的下標,即high位。high位=low位+原雜湊桶容量
                //低位連結串列的頭結點、尾節點
                Node<K,V> loHead = null, loTail = null;
                //高位連結串列的頭節點、尾節點
                Node<K,V> hiHead = null, hiTail = null;
                Node<K,V> next;//臨時節點 存放e的下一個節點
                do {
                    next = e.next;
                  //利用位運算代替常規運算:利用雜湊值與舊的容量,可以得到雜湊值去模後,是大於等於oldCap還是小於oldCap,等於0代表小於oldCap,應該存放在低位,否則存放在高位
                    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);
                    //將低位連結串列存放在原index處
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    //將高位連結串列存放在新index處
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

resize的操作主要涉及以下幾步操作:

  1. 如果到達最大容量,那麼返回當前的桶,並不再進行擴容操作,否則的話擴容為原來的兩倍,返回擴容後的桶;
  2. 根據擴容後的桶,修改其他的成員變數的屬性值;
  3. 根據新的容量建立新的擴建後的桶,並更新桶的引用;
  4. 如果原來的桶裡面有元素就需要進行元素的轉移;
  5. 在進行元素轉移的時候需要考慮到元素碰撞和轉紅黑樹操作;
  6. 在擴容的過程中,按次從原來的桶中取出連結串列頭節點,並對該連結串列上的所有元素重新計算hash值進行分配;
  7. 在發生碰撞的時候,將新加入的元素新增到末尾;
  8. 在元素複製的時候需要同時對低位和高位進行操作。

四、成員方法

  • put方法:
//向雜湊表中新增元素
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

向用戶開放的put方法呼叫的是putVal方法:

putVal方法需要判斷是否出現雜湊衝突問題:

其中如果雜湊值相等,key也相等,則是覆蓋value操作;如果不是覆蓋操作,則插入一個普通連結串列節點;

遍歷到尾部,追加新節點到尾部;

在元素新增的過程中需要隨時檢查是否需要進行轉換成紅黑樹的操作;

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    //tab存放當前的雜湊桶,p用作臨時連結串列節點  
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //如果當前雜湊表是空的,代表是初始化
    if ((tab = table) == null || (n = tab.length) == 0)
    //那麼直接去擴容雜湊表,並且將擴容後的雜湊桶長度賦值給n
    n = (tab = resize()).length;
    //如果當前index的節點是空的,表示沒有發生雜湊碰撞。直接構建一個新節點Node,掛載在index處即可。
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {//否則 發生了雜湊衝突。
        Node<K,V> e; K k;
        //如果雜湊值相等,key也相等,則是覆蓋value操作
        if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
            e = p;//將當前節點引用賦值給e
        else if (p instance of 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;
                }
                //如果找到了要覆蓋的節點
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //如果e不是null,說明有需要覆蓋的節點,
        if (e != null) { // existing mapping for key
            //則覆蓋節點值,並返回原oldValue
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            //這是一個空實現的函式,用作LinkedHashMap重寫使用。
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //如果執行到了這裡,說明插入了一個新的節點,所以會修改modCount,以及返回null。
    ++modCount;
    //更新size,並判斷是否需要擴容。
    if (++size > threshold)
    resize();
    //這是一個空實現的函式,用作LinkedHashMap重寫使用。
    afterNodeInsertion(evict);
    return null;
}

當存入的key是null的時候將呼叫putVal方法,看key不為null的情況。先呼叫了hash(int h)方法獲取了一個hash值。

 “擾動函式”,這個方法的主要作用是防止質量較差的雜湊函式帶來過多的衝突(碰撞)問題。Java中int值佔4個位元組,即32位。根據這32位值進行移位、異或運算得到一個值。

那HashMap中最核心的部分就是雜湊函式,又稱雜湊函式。也就是說,雜湊函式是通過把key的hash值對映到陣列中的一個位置來進行訪問。

hashCode右移16位,正好是32bit的一半。與自己本身做異或操作(相同為0,不同為1)。就是為了混合雜湊值的高位和低位,增加低位的隨機性。並且混合後的值也變相保持了高位的特徵。

HashMap之所以不能保持元素的順序有以下幾點原因:

  1. 插入元素的時候對元素進行雜湊處理,不同元素分配到table的不同位置;
  2. 容量拓展的時候又進行了hash處理;第三,複製原表內容的時候連結串列被倒置。
  3. 複製原表內容的時候連結串列被倒置。
//只做一次16位右位移異或混合:
static int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h=key.hashCode())^(h>>>16);
}

其中,key.hashCode()是Key自帶的hashCode()方法,返回一個int型別的雜湊值。我們大家知道,32位帶符號的int表值範圍從-2147483648到2147483648。這樣只要hash函式鬆散的話,一般是很難發生碰撞的,因為HashMap的初始容量只有16。但是這樣的雜湊值我們是不能直接拿來用的。用之前需要對陣列的長度取模運算。得到餘數才是索引值。

indexFor返回hash值和table陣列長度減1的與運算結果。為什麼使用的是length-1?因為這樣可以保證結果的最大值是length-1,不會產生陣列越界問題。

static int indexFor(int h, int length) {
    return h & (length-1);
}
  • get方法
public V get(Object key) {
    Node<K,V> e;
    //傳入擾動後的雜湊值 和 key 找到目標節點Node
    return (e = getNode(hash(key), key)) == null ? null : e.value; 
}

HashMap向用戶分開放的get方法是呼叫的getNode方法來實現的,

//傳入擾動後的雜湊值 和 key 找到目標節點Node
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //查詢過程,找到返回節點,否則返回null
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

查詢的判斷條件是:e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))),在比較hash值的同時需要比較key的值是否相同。

  • 其他方法

contains()

 HashMap沒有提供判斷元素是否存在的方法,只提供了判斷Key是否存在及Value是否存在的方法,分別是containsKey(Object key)、containsValue(Object value)。
containsKey(Object key)方法很簡單,只是判斷getNode (key)的結果是否為null,是則返回false,否返回true。
//傳入擾動後的雜湊值 和 key 找到目標節點Node
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //查詢過程,找到返回節點,否則返回null
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
  • 判斷一個value是否存在比判斷key是否存在還要簡單,就是遍歷所有元素判斷是否有相等的值。這裡分為兩種情況處理,value為null何不為null的情況,但內容差不多,只是判斷相等的方式不同。這個判斷是否存在必須遍歷所有元素,是一個雙重迴圈的過程,因此是比較耗時的操作。

remove方法

HashMap中“刪除”相關的操作,有remove(Object key)和clear()兩個方法。
其中向用戶開放的remove方法呼叫的是removeNode方法,,removeNode (key)的返回結果應該是被移除的元素,如果不存在這個元素則返回為null。remove方法根據removeEntryKey返回的結果e是否為null返回null或e.value。
public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; 
}

final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
    // p 是待刪除節點的前置節點
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    //如果雜湊表不為空,則根據hash值算出的index下 有節點的話。
    if ((tab = table) != null && (n = tab.length) > 0&&(p = tab[index = (n - 1) & hash]) != null) {
        //node是待刪除節點
        Node<K,V> node = null, e; K k; V v;
        //如果連結串列頭的就是需要刪除的節點
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;//將待刪除節點引用賦給node
        else if ((e = p.next) != null) {//否則迴圈遍歷 找到待刪除節點,賦值給node
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                do {
                    if (e.hash == hash && ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        //如果有待刪除節點node,  且 matchValue為false,或者值也相等
        if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)//如果node == p,說明是連結串列頭是待刪除節點
                tab[index] = node.next;
            else//否則待刪除節點在表中間
                p.next = node.next;
            ++modCount;//修改modCount
            --size;//修改size
            afterNodeRemoval(node);//LinkedHashMap回撥函式
            return node;
        }
    }
    return null;
}

clear()

clear()方法刪除HashMap中所有的元素,這裡就不用一個個刪除節點了,而是直接將table陣列內容都置空,這樣所有的連結串列都已經無法訪問,Java的垃圾回收機制會去處理這些連結串列。table陣列置空後修改size為0。

public void clear() {
    Node<K,V>[] tab;
    modCount++;
    if ((tab = table) != null && size > 0) {
        size = 0;
        for (int i = 0; i < tab.length; ++i)
            tab[i] = null;
    }
}

五、樹形化和紅黑樹的操作

可以看到無論是put,get還是remove方法中都有if (node instanceof TreeNode)方法來判斷當前節點是否是一個樹形化的節點,如果是的話就需要呼叫相應的紅黑樹的相關操作。

  • 紅黑樹的成員變數的定義:
TreeNode<K,V> parent;  // 父節點
TreeNode<K,V> left; //左節點
TreeNode<K,V> right; //右節點
TreeNode<K,V> prev;    // 在連結串列中的前一個節點
boolean red; //染紅或者染黑標記
  • 桶的樹形化

  桶的樹形化 treeifyBin(),如果一個桶中的元素個數超過 TREEIFY_THRESHOLD(預設是 8 ),就使用紅黑樹來替換連結串列,從而提高速度。這個替換的方法叫 treeifyBin() 即樹形化。

//將桶內所有的 連結串列節點 替換成 紅黑樹節點
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    //如果當前雜湊表為空,或者雜湊表中元素的個數小於進行樹形化的閾值(預設為 64),就去新建/擴容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    //如果雜湊表中的元素個數超過了樹形化閾值,進行樹形化,e是雜湊表中指定位置桶裡的連結串列節點,從第一個開始
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        //新建一個樹形節點,內容和當前連結串列節點e一致
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        //讓桶的第一個元素指向新建的紅黑樹頭結點,以後這個桶裡的元素就是紅黑樹而不是連結串列了
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

這段程式碼很簡單,只是對桶裡面的每個元素呼叫了replacementTreeNode方法將當前的節點變為一個樹形節點來進行樹形化:

TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
    return new TreeNode<>(p.hash, p.key, p.value, next);
}

在所有的節點都替換成樹形節點後需要讓桶的第一個元素指向新建的紅黑樹頭結點,以後這個桶裡的元素就是紅黑樹而不是連結串列了,之前的操作並沒有設定紅黑樹的顏色值,現在得到的只能算是個二叉樹。在最後呼叫樹形節點 hd.treeify(tab) 方法進行塑造紅黑樹,這是HashMap中個人認為第二個比較難的方法:

final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null;
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
        next = (TreeNode<K,V>)x.next;
        x.left = x.right = null;
        if (root == null) { //第一次進入迴圈,確定頭結點,為黑色
            x.parent = null;
            x.red = false;
            root = x;
        }
        else {  //後面進入迴圈走的邏輯,x 指向樹中的某個節點
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            //又一個迴圈,從根節點開始,遍歷所有節點跟當前節點 x 比較,調整位置,有點像氣泡排序
            for (TreeNode<K,V> p = root;;) {
                int dir, ph;        //這個 dir 
                K pk = p.key;
                if ((ph = p.hash) > h)  //當比較節點的雜湊值比 x 大時,dir 為 -1
                    dir = -1;
                else if (ph < h)  //雜湊值比 x 小時 dir 為 1
                    dir = 1;
                else if ((kc == null && (kc = comparableClassFor(k)) == null) ||(dir = compareComparables(kc, k, pk)) == 0)
                    // 如果比較節點的雜湊值x 
                    dir = tieBreakOrder(k, pk);
                //把當前節點變成 x 的父親
                //如果當前比較節點的雜湊值比 x 大,x 就是左孩子,否則 x 是右孩子 
                TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp;
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    //修正紅黑樹
                    root = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }
    moveRootToFront(tab, root);
}

  可以看到,將二叉樹變為紅黑樹時,需要保證有序。這裡有個雙重迴圈,拿樹中的所有節點和當前節點的雜湊值進行對比(如果雜湊值相等,就對比鍵,這裡不用完全有序),然後根據比較結果確定在樹種的位置。

  紅黑樹的基本要求:

  紅黑樹是一種近似平衡的二叉查詢樹,它能夠確保任何一個節點的左右子樹的高度差不會超過二者中較低那個的一倍。它不是嚴格控制左、右子樹高度或節點數之差小於等於1,但紅黑樹高度依然是平均log(n),且最壞情況高度不會超過2log(n)。紅黑樹是滿足如下條件的二叉查詢樹(binary search tree):

  1. 每個節點要麼是紅色,要麼是黑色。
  2. 根節點必須是黑色
  3. 紅色節點不能連續(也即是,紅色節點的孩子和父親都不能是紅色)。
  4. 對於每個節點,從該點至null(樹尾端)的任何路徑,都含有相同個數的黑色節點。

上面的方法treeify涉及到的修正紅黑樹的方法balanceInsertion方法需要對樹中節點進行重新的染色,這個函式也是紅黑樹樹插入資料時需要呼叫的函式,其中涉及到的是左旋和右旋操作,這也是紅黑樹中兩個主要的操作:

static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) {
    //插入的節點必須是紅色的,除非是根節點
    x.red = true;
    //遍歷到x節點為黑色,整個過程是一個上濾的過程
    xp=x.parent;xpp=xp.parent;xppl=xpp.left;xppr=xpp.right;
    for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
        if ((xp = x.parent) == null) {
            x.red = false;
            return x;
        }
        //如果xp是黑色就直接完成,最簡單的情況
        else if (!xp.red || (xpp = xp.parent) == null)
            return root;
        //如果x的父節點是xp父節點的左節點
        if (xp == (xppl = xpp.left)) {
            //x的父親節點的兄弟是紅色的(需要顏色翻轉)case1
            if ((xppr = xpp.right) != null && xppr.red) {
                xppr.red = false; //x父親節點的兄弟節點置成黑色
                xp.red = false; //父親和其兄弟節點一樣是黑色
                xpp.red = true; //祖父節點置成紅色
                x = xpp; //然後上濾(就是不斷的重複上面的操作)
            }
            else {
                //如果x是xp的右節點整個要進行兩次旋轉,先左旋轉再右旋轉
                // case2
                if (x == xp.right) {
                    root = rotateLeft(root, x = xp);//左旋
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                //case3
                if (xp != null) {
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        root = rotateRight(root, xpp);//右旋
                    }
                }
            }
        }
        //以左節點映象對稱
        else {
            if (xppl != null && xppl.red) {
                xppl.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            else {
                if (x == xp.left) {
                    root = rotateRight(root, x = xp);
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) {
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        root = rotateLeft(root, xpp);
                    }
                }
            }
        }
    }
}

左旋操作和右旋操作,拿出其中的程式碼:

   左旋轉 
 static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root, TreeNode<K,V> p) {  
           TreeNode<K,V> r, pp, rl;  
           if (p != null && (r = p.right) != null) {  
               if ((rl = p.right = r.left) != null)  
                   rl.parent = p;  
               if ((pp = r.parent = p.parent) == null)  
                   (root = r).red = false;  
               else if (pp.left == p)  
                   pp.left = r;  
               else  
                   pp.right = r;  
               r.left = p;  
               p.parent = r;  
           }  
           return root;  
       }  
  
右旋轉  
  static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root, TreeNode<K,V> p) {  
           TreeNode<K,V> l, pp, lr;  
           if (p != null && (l = p.left) != null) {  
               if ((lr = p.left = l.right) != null)  
                   lr.parent = p;  
               if ((pp = l.parent = p.parent) == null)  
                   (root = l).red = false;  
               else if (pp.right == p)  
                   pp.right = l;  
               else  
                   pp.left = l;  
               l.right = p;  
               p.parent = l;  
           }  
           return root;  
       }

左旋

左旋的過程是將x的右子樹繞x逆時針旋轉,使得x的右子樹成為x的父親,同時修改相關節點的引用。旋轉之後,二叉查詢樹的屬性仍然滿足。

TreeMap_rotateLeft.png

右旋

右旋的過程是將x的左子樹繞x順時針旋轉,使得x的左子樹成為x的父親,同時修改相關節點的引用。旋轉之後,二叉查詢樹的屬性仍然滿足。

TreeMap_rotateRight.png

針對HashMap的樹形結構的插入,刪除,查詢操作也與資料結構中紅黑樹的操作是類似的,瞭解紅黑樹的操作也就瞭解了HashMap的樹形結構的操作,balanceInsertion和左旋右旋的操作是上述HashMap的樹形結構操作的關鍵。

併發下HashMap死鎖原因及替代方案

多執行緒下[HashMap]的問題:

1、多執行緒put操作後,get操作導致死迴圈。
2、多執行緒put非NULL元素後,get操作得到NULL值。
3、多執行緒put操作,導致元素丟失。

主要原因發生在多執行緒訪問HashMap時,同時放入元素,導致擴容時候,重新分配hash地址,將舊的Node元素轉移到新的table中時,出現迴圈引用的(出現環)的情況,導致get()方法,呼叫時,cpu佔用100%的情況。

HashMap其實並不是執行緒安全的,在高併發的情況下,是很可能發生死迴圈的,由此造成CPU 100%,這是很可怕的,所以在多執行緒的情況下,用HashMap是很不妥當的行為,應採用執行緒安全類ConcurrentHashMap進行代替。

HashMap進行儲存時,如果size超過當前最大容量*負載因子時候會發生resize,首先看一下resize原始碼

void resize(int newCapacity) {

Entry[] oldTable = table;

int oldCapacity = oldTable.length;

if (oldCapacity == MAXIMUM_CAPACITY) {

threshold = Integer.MAX_VALUE;

return;

}


Entry[] newTable = new Entry[newCapacity];

transfer(newTable);

table = newTable;

threshold = (int)(newCapacity * loadFactor);

}


而這段程式碼中又呼叫了transfer()方法,而這個方法實現的機制就是將每個連結串列轉化到新連結串列,並且連結串列中的位置發生反轉,而這在多執行緒情況下是很容易造成連結串列迴路,從而發生get()死迴圈,我們看一下他的原始碼

void transfer(Entry[] newTable) {

Entry[] src = table;

int newCapacity = newTable.length;

for (int j = 0; j < src.length; j++) {

Entry<K,V> e = src[j];

if (e != null) {

src[j] = null;

do {

Entry<K,V> next = e.next;

int i = indexFor(e.hash, newCapacity);

e.next = newTable[i];

newTable[i] = e;

e = next;

} while (e != null);

}

}

}

HashMap死迴圈演示

假如有兩個執行緒P1、P2,以及連結串列 a=》b=》null

1、P1先執行,執行完"Entry<K,V> next = e.next;"程式碼後發生阻塞,或者其他情況不再執行下去,此時e=a,next=b

2、而P2已經執行完整段程式碼,於是當前的新連結串列newTable[i]為b=》a=》null

3、P1又繼續執行"Entry<K,V> next = e.next;"之後的程式碼,則執行完"e=next;"後,newTable[i]為a《=》b,則造成迴路,while(e!=null)一直死迴圈

總結

HashMap並非執行緒安全,所以在多執行緒情況下,應該首先考慮用ConcurrentHashMap,避免悲劇的發生

相關推薦

集合框架HashMap原理原始碼解讀

本文加上個人理解,用自己的話表達集合框架及對HashMap細節的理解。 簡介 HashMap是一種利用鍵值對映儲存資料的資料結構,隨著jdk的發展,在jdk1.8中引入了紅黑樹的資料結構和擴容的優化。 Map類常用集合介紹 HashMap實現自java.uti

集合框架JDK1.8源碼分析之HashMap(一) 轉載

.get 修改 object set implement .com 功能 數組元素 帶來 一、前言   在分析jdk1.8後的HashMap源碼時,發現網上好多分析都是基於之前的jdk,而Java8的HashMap對之前做了較大的優化,其中最重要的一個優化就是桶中

ArrayList集合(JDK1.8) 集合框架JDK1.8原始碼分析之ArrayList(六)

簡述   List是繼承於Collection介面,除了Collection通用的方法以外,擴充套件了部分只屬於List的方法。   常用子類  ?ArrayList介紹 1.資料結構   其底層的資料結構是陣列,陣列元素型別為Object型別,即可以存放所

集合框架之深入分析HashMap

提出並解決問題如下: 問題1:初始容量為什麼是16,為什麼必須是2的冪? 問題2: hash方法為什麼是無符號右移16位? 問題3: 問題4: 問題5: HashMap 非執行緒安全 繼承於AbstractMap 實現了Map、Clon

讀書筆記Cronjob原理源碼分析

之前 jobs 所有 res net pes 垃圾回收gc ive 發現 原文鏈接:https://mp.weixin.qq.com/s?__biz=MzI0NjI4MDg5MQ==&mid=2715291842&idx=1&sn=e605f9b40

基礎+實戰JVM原理優化系列之八:如何檢視JVM引數配置?

1. 檢視JAVA版本資訊 2. 檢視JVM執行模式  在$JAVA_HOME/jre/bin下有client和server兩個目錄,分別代表JVM的兩種執行模式。   client執行模式,針對桌面應用,載入速度比server模式快10%,而執行速度為server模

資料結構HashTable原理實現學習總結

有兩個類都提供了一個多種用途的hashTable機制,他們都可以將可以key和value結合起來構成鍵值對通過put(key,value)方法儲存起來,然後通過get(key)方法獲取相對應的value值。一個是前面提到的HashMap,還有一個就是馬上要講解的HashTa

資料結構LinkedList原理實現學習總結

一、LinkedList實現原理概述 LinkedList 和 ArrayList 一樣,都實現了 List 介面,但其內部的資料結構有本質的不同。LinkedList 是基於連結串列實現的(通過名字也能區分開來),所以它的插入和刪除操作比 ArrayList

HashMap和ConcurrentHashMap原理原始碼解讀

前言 Map 這樣的 Key Value 在軟體開發中是非常經典的結構,常用於在記憶體中存放資料。 本篇主要想討論 ConcurrentHashMap 這樣一個併發容器,在正式開始之前我覺得有必要談談 HashMap,沒有它就不會有後面的 ConcurrentHashMa

雜湊、HashMap原理原始碼、Hash的一些應用面試題

一、雜湊定義     Hash,一般翻譯做“雜湊”,也有直接音譯為"雜湊"的,就是把任意長度的輸入(又叫做預對映, pre-image),通過雜湊演算法,變換成固定長度的輸出,該輸出就是雜湊值。這種轉換是一種壓縮對映,也就是,雜湊值的空間通常遠小於輸入的空間,不 同的輸入可

Learning NotesCTC 原理實現

CTC( Connectionist Temporal Classification,連線時序分類)是一種用於序列建模的工具,其核心是定義了特殊的目標函式/優化準則[1]。 jupyter notebook 版見 repo. 1. 演算法 這裡

特徵匹配ORB原理原始碼解析

相關 : CSDN-勿在浮沙築高臺   為了滿足實時性的要求,前面文章中介紹過快速提取特徵點演算法Fast,以及特徵描述子Brief。本篇文章介紹的ORB演算法結合了Fast和Brief的速度優勢,並做了改進,且ORB是免費。 Ethan Rublee等人2011年

Vue.use原理原始碼解讀

vue.use(plugin, arguments) 語法 引數:plugin(Function | Object) 用法: 如果vue安裝的元件型別必須為Function或者是Object<br/>如果是個物件,必須提供install方法 如果是一個函式,會被直接當作install

spring的配置載入原理原始碼解讀

spring的配置是怎樣載入的,載入配置的同時都幹了什麼,配置的先載入後加載造成的影響 1、spring 配置載入 (圖1.1.1) spring的配置資訊是在spring refresh方法時候在建立beanFactory的時候呼叫的。 (圖1

2,MapReduce原理原始碼解讀

# MapReduce原理及原始碼解讀 [TOC] ### 一、分片 #### 靈魂拷問:為什麼要分片? - **分而治之:**MapReduce(MR)的核心思想就是分而治之;何時分,如何分就要從原理和原始碼來入手。做為碼農大家都知道,不管一個程式多麼複雜,在寫程式碼和學習程式碼之前最重要的就是

java基礎ConcurrentHashMap實現原理原始碼分析

  ConcurrentHashMap是Java併發包中提供的一個執行緒安全且高效的HashMap實現(若對HashMap的實現原理還不甚瞭解,可參考我的另一篇文章),ConcurrentHashMap在併發程式設計的場景中使用頻率非常之高,本文就來分析下Concurre

特徵匹配HarrisShi-Tomasi原理原始碼解析

演算法原理:呼叫cornerMinEigenVal()函式求出每個畫素點自適應矩陣M的較小特徵值,儲存在矩陣eig中,然後找到矩陣eig中最大的畫素值記為maxVal,然後閾值處理,小於qualityLevel*maxVal的特徵值排除掉,最後函式確保所有發現的角點之間具有足夠的距離。void cv::goo

MyBatisMyBatis Tomcat JNDI原理原始碼分析

一、 Tomcat JNDI JNDI(java nameing and drectory interface),是一組在Java應用中訪問命名和服務的API,所謂命名服務,即將物件和名稱聯絡起來,使得可以通過名稱訪問並獲取物件。 簡單

特徵匹配BRIEF特徵描述子原理原始碼解析

轉載請註明出處: http://blog.csdn.net/luoshixian099/article/details/48338273   傳統的特徵點描述子如SIFT,SURF描述子,每個特徵點採用128維(SIFT)或者64維(SURF)向量去描述,每個維度上

數據結構ArrayList原理實現學習總結(2)

!= 需要 但是 object count def 原理 arrays 位置 ArrayList是一個基於數組實現的鏈表(List),這一點可以從源碼中看出: transient Object[] elementData; // non-private to si