HashMap源碼解析

HashMap、HashTable、ConcurrentHashMap總結:

本篇博文將會大篇幅介紹 JDK1.8 HashMap,下面總結:

HashMap:
  1. JDK1.7底層是 數組 + 鏈表實現的, JDK1.8添加了紅黑樹,節點達到一定條件之後,鏈表和紅黑樹之間存在相互轉化的場景
  2. key 不可以重複,但可以爲null,value值不做限定。
  3. HashMap數組初始化size=16,每次擴容爲2倍,size一定爲2的n次冪【初始化時傳入的size,會被轉化爲2的n次冪】
  4. 默認情況下,當Map中元素總數超過Entry數組的75%,觸發擴容操作,爲了減少鏈表長度,元素分配更均勻
  5. 哈希衝突:若干Key的哈希值按數組大小取模後【hash & (tab.length – 1)】,如果落在同一個數組下標上,將組成一條Entry鏈【紅黑樹】,對Key的查找需要遍歷Entry鏈上的每個元素執行equals()比較
  6. 加載因子:爲了降低哈希衝突的概率,默認當HashMap中的鍵值對達到數組大小的75%時,即會觸發擴容
  7. 空間換時間:如果希望加快Key查找的時間,還可以進一步降低加載因子,加大初始大小,以降低哈希衝突的概率

HashTable:

  1. 底層數組+鏈表實現,無論key還是value都不能爲null,線程安全,實現線程安全的方式是在修改數據時鎖住整個HashTable,效率低,ConcurrentHashMap做了相關優化
  2. 初始size爲11,擴容:newsize = olesize*2+1
  3. 計算index的方法:index = (hash & 0x7FFFFFFF) % tab.length

ConcurrentHashMap:

  1. ConcurrentHashMap使用了鎖分離的技術實現線程安全,可以完全替代HashTable,在併發編程的場景中使用頻率非常之高。
  2. JDK1.7 ConcurrentHashMap 是使用Segment段來進行加鎖,個段就相當於一個HashMap的數據結構,每個段使用一個鎖, JDK1.8之後Segment雖保留,但已經簡化屬性,僅僅是爲了兼容舊版本,使用和HashMap一樣的數據結構每個數組位置使用一個鎖。

HashMap源碼解析:

上面我們已經講了 HashMap 是數組、鏈表、紅黑樹組成的,我們把他抽象成一個圖可以看到,一定要記住這個圖片,其中每個方塊是一個節點,每個節點裏面的字母是 key:
在這裏插入圖片描述
打開 HashMap的源碼會發現其中定義了很多變量,其中有幾個比較重要的變量:

  1. Node<K,V>[] table : node 類型的數組,作爲HashMap的三大結構之一
  2. int size: 已儲存元素的個數
  3. int threshold: 擴容的閾值,當 HashMap的size大於threshold時會執行resize操作。 threshold=table .length * loadFactor
  4. float loadFactor: 負載因子, 用來計算 threshold

	// 默認初始容量-須是2的冪 這裏是 2的4次方16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    // HashMap 最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;

    // 默認負載係數 (HashMap擴容是使用)
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    // 鏈表轉化爲紅黑樹的閾值
    static final int TREEIFY_THRESHOLD = 8;

    // 紅黑樹中節點個數轉爲鏈表的閾值
    static final int UNTREEIFY_THRESHOLD = 6;

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

	// HashMap的三大數據結構之一 數組。
    transient Node<K,V>[] table;

    // Entry 的set集合 遍歷時使用
    transient Set<Map.Entry<K,V>> entrySet;

    // 已經存儲的數據容量
    transient int size;

    // 被修改的次數
    transient int modCount;

	 /**
     * 擴容的閾值
     * 1、當 HashMap的size大於threshold時會執行resize操作。
     * 2、threshold=capacity*loadFactor
     */
    int threshold;

    // 負載因子參數
    final float loadFactor;

初始化方法,其中推薦的 第二種,當我們預先知道元素的個數時, 可以有效的避免擴容。


	// 無參初始化, 默認容量時16,負載因子 = DEFAULT_LOAD_FACTOR = 0.75f
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
	
	/**
     * 帶參數的初始化。【推薦,可以避免擴容】
     *
     * 注意:HashMap推薦initialCapacit最好是2的n次冪,有利於均勻分佈數組的下標。
     *      不過放心在tableSizeFor已經幫我們處理好了
     * @param initialCapacity 初始化的容量
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
	
	 /**
     * 帶參數的初始化
     * @param initialCapacity 初始化的容量
     * @param loadFactor 負載因子
     */
    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;
        // 設置第一次需要擴容時的閾值,此時由 initialCapacity 決定的,和其他數據無關
        this.threshold = tableSizeFor(initialCapacity);
    }

	//  返回比給定目標 大的 2的n次冪數。
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

重點 put方法:

我們初始化 HashMap 的時候,並沒有創建數組,僅僅是對一些參數進行賦值:

  1. 在我們put 一個元素的時候 判斷 table是否已經初始化,並將其初始化,是否達到擴容閾值,進行擴容。
  2. 計算 key 的hash值 並與數組的長度做:(n - 1) & hash 計算,計算key 在數組上的下標位置。
    a. 如果爲空,直接加入
    b.如果爲鏈表,放在最後,並判斷是否進行 鏈表轉化爲 紅黑樹
    c.如果爲紅黑樹,則放在紅黑樹裏,並對紅黑樹進行修復
    d.如果在放入數組、鏈表、紅黑樹時,找到相同的 key 則將其原先的 Value 替代。
  3. put 方法源碼:
	
	// put 鍵值對到Map中 【重要】
    public V put(K key, V value) {
        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;
        // 通過(n - 1) & hash計算其在數組的下標位【目的可以使元素均勻的分佈在數組上】
        // 如果當前數據位置沒有元素,就放在這個位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            // 當前key對應的節點
            Node<K,V> e;
            K k;
            // 我們保存的 key 與數組位置的key重複,最後面會把value值替代
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            /**
             * 當前節點是紅黑樹:
             * 1、如果紅黑樹中存在相同的 key 返回該節點
             * 2、如果紅黑樹中不存在相同的 key,將key-value生成新的節點,放入到紅黑樹中,返回null
             * 由於博主對紅黑樹理解不夠透徹,暫時不去解讀putTreeVal方法
             */
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            /**
             * 當前節點是鏈表
             * 1、遍歷鏈表,鏈表中存在相同的 key 返回該節點
             * 2、鏈表中不存在相同的 key,將key-value生成新的節點,放入到鏈表最後,
             */
            else {
                // 遍歷鏈表
                for (int binCount = 0; ; ++binCount) {
                    // 到了鏈表末端
                    if ((e = p.next) == null) {
                        // 生成新的節點,賦值到鏈表最後
                        p.next = newNode(hash, key, value, null);
                        // 判斷是否將當前節點 鏈表轉紅黑樹
                        // 條件1:鏈表的個數 >= TREEIFY_THRESHOLD - 1
                        // 條件2: 數組的大小 < MIN_TREEIFY_CAPACITY, 滿足條件1,不滿足條件2會進行擴容
                        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;
                }
            }
            // 表示在key存在於 HashMap中,將值進行更新
            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;
    }
	
	/**
     *初始化或加倍表大小。如果爲空,則分配
     *與初始容量目標保持一致。
     *否則,因爲我們使用的是二次展開的冪,所以
     *每個bin中的元素必須保持在同一索引中,或者移動
     *在新表中使用兩個偏移量的冪。
     *
     *@return table
     */
    final Node<K,V>[] resize() {
        // 保存數據的 數組
        Node<K,V>[] oldTab = table;
        // 獲取數據長度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 舊的擴容閾值
        int oldThr = threshold;

        // 擴容後 數組長度
        int newCap = 0;
        // 擴容後 下一次在擴容的閾值
        int newThr = 0;
        // 原來數組已經初始化
        if (oldCap > 0) {
            // 容量已經達到最大,不進行處理了
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 將原來的數組容量(oldCap)擴大一倍,賦予新的數組容量(newCap)
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                // 將擴容閾值(oldThr)擴大一倍,賦予新的擴容閾值(newThr)
                newThr = oldThr << 1; // double threshold
        }
        // 初始容量設置爲閾值
        else if (oldThr > 0)
            newCap = oldThr;
        else {
            // 初始化數組容量
            newCap = DEFAULT_INITIAL_CAPACITY;
            // 初始化 擴容閾值,這裏可以看出, 擴容閾值 = 負載因子 * 數組的長度
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 生成新的擴容閾值
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);
        }
        // 賦值新的擴容閾值
        threshold = newThr;


        // 定義一個新的數組長度爲 newCap;
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        // 數據遷移
        if (oldTab != null) {
            // 先遍歷老的數組
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                // 拿到原先數組下標的數據
                if ((e = oldTab[j]) != null) {
                    // 位置置空,幫助GC
                    oldTab[j] = null;

                    // 當前位置元素 沒有下一個節點
                    if (e.next == null)
                        // 根據當前元素的 hash 和新數組的大小,計算元素E在新的數組中的位置
                        // 爲什麼要 e.hash & (newCap - 1) 計算下標? 因爲這樣會使得元素在數組上的分佈更加均勻
                        newTab[e.hash & (newCap - 1)] = e;
                    /**
                     * 先看下面鏈表的遷移方式,在看紅黑樹的遷移方式 【注意】
                     */
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    /**
                     * 鏈表的遷移方式
                     * 1、 & 運算規則:兩個數都轉爲二進制,然後從高位開始比較,如果兩個數都爲1則爲1,否則爲0。
                     * 2、put方法計算時(oldCap - 1) & e.hash < oldCap,保證了結果始終在數組範圍內。
                     * 3、擴容時:oldCap & e.hash = 0表示當前的key是屬於原來數組範圍內,oldCap & e.hash != 0 表示在擴容的範圍內
                     * 4、根據上面的計算結果,定義低位和高位兩個鏈表,最後複製到新的數組中newTab
                     * 5、這也是數組每次擴容兩倍的原因
                     * 【這裏需要仔細的琢磨琢磨!!!!】
                     */
                    else {
                        // 低位鏈表的頭和尾節點
                        Node<K,V> loHead = null, loTail = null;
                        // 高位鏈表的頭和尾節點
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        // 根據hash值 將當前節點下的鏈表分爲高位和低位鏈表
                        do {
                            next = e.next;
                            // 低位鏈表
                            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;
                        }
                        // 將高位鏈表賦值到(j + oldCap)位置,
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
	
	// 是否將鏈表轉爲紅黑樹
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        // 數組的大小 < MIN_TREEIFY_CAPACITY, 會進行擴容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        // 執行樹化
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            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);
        }
    }

put 方法中一些註釋做了一些解釋,最主要的還是在 resize() 擴容方法,在數組、鏈表、以及紅黑樹中查找位置的代碼非常的重要,由於博主對紅黑樹研究不夠深入,暫時沒進行解析。

get方法:

看完 put 方法在看 get 方法就非常簡單
1. 先計算在數組上的位置: (n - 1) & hash
2. 查看數組上節點key值是否相同
3. 然後判斷 是鏈表 還是紅黑樹,進行遍歷,如果沒有找到相同的key 則返回 null


// 獲取Values 值
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    // 根據 key的hash 和 key的值獲取節點
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        // 根據 (n - 1) & hash 找到對應的下標位,是否有元素
        if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {
            // 數組下表位元素key 與 傳入的key相同,直接返回該節點
            if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            // 查找first後的元素
            if ((e = first.next) != null) {
                // first 是紅黑樹根節點
                if (first instanceof TreeNode)
                    // 遍歷紅黑樹返回節點
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // first 是鏈表
                do {
                    // 遍歷鏈表返回節點
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

總結:

HashMap 首先需要了解其三種數據結構,再瞭解其擴容的原理,以及鏈表和紅黑樹之間的轉換,基本上就OK 了;

Hashtable

Hashtable 其實是 HashMap 的前身,Hashtable是線程安全的,它的組成結構,只有數組和鏈表並在很多修改數據的方法上加入了 synchronized 來保證線程安全性:
put 和 get 代碼如下:


public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }
        // 數組
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        // 計算下標位置
        int index = (hash & 0x7FFFFFFF) % tab.length;
        // 獲取下標位置的元素
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        // 下標元素和其後面的鏈表是否與key重複,存在則將value替代,並返回
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
        // 添加一個節點
        addEntry(hash, key, value, index);
        return null;
    }
	
	// Get
    public synchronized V get(Object key) {
        // 數組
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        // 計算下標
        int index = (hash & 0x7FFFFFFF) % tab.length;
        // 查詢當前的下標是否有包含key的數據 【當前下標位置的元素可能的值:null,節點,鏈表】
        for (Entry<?,?> e = tab[index]; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return (V)e.value;
            }
        }
        return null;
    }

	public synchronized int size() {
        return count;
    }
	
	public synchronized boolean isEmpty() {
        return count == 0;
    }
ConcurrentHashMap

相比 Hashtable,ConcurrentHashMap 採用了分段鎖的概念,它的數據結構和HashMap基本相同,採用了 CAS 原子操作修改一些屬性和元素避免併發問題。並 synchronized 對數組的每個下標位置元素進行加鎖,保證當前節點桶【鏈表、紅黑樹】 併發安全,
我們來簡單的看一下 put 的代碼:


// put
    public V put(K key, V value) {
        return putVal(key, value, false);
    }


    final V putVal(K key, V value, boolean onlyIfAbsent) {
        // key、value 不能爲空
        if (key == null || value == null) throw new NullPointerException();
        // 獲得key的hash值
        int hash = spread(key.hashCode());
        // 用來計算在這個節點總共有多少個元素,用來控制擴容或者轉移爲樹
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            // 數組爲空,初始化數組
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            // 根據 (n - 1) & hash 獲取當前位置的節點
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                // 通過CAS線程安全,將當前的節點放置到數組指定位置
                if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            // ----------------------------
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                // 給當前的節點加鎖,保證操作當前的節點線程安全
                synchronized (f) {
                    //  再次取出要存儲的位置的元素,跟前面取出來的比較
                    if (tabAt(tab, i) == f) {
                        //  取出來的元素的hash值大於0,當轉換爲樹之後,hash值爲-2
                        if (fh >= 0) {
                            binCount = 1;
                            //遍歷這個鏈表
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //  要存的元素的hash,key跟要存儲的位置的節點的相同的時候,替換掉該節點的value即可
                                if (e.hash == hash &&
                                        ((ek = e.key) == key ||
                                                (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    //當使用putIfAbsent的時候,只有在這個key沒有設置值得時候才設置
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                //如果不是同樣的hash,同樣的key的時候,則判斷該節點的下一個節點是否爲空,
                                if ((e = e.next) == null) {
                                    //爲空的話把這個要加入的節點設置爲當前節點的下一個節點
                                    pred.next = new Node<K,V>(hash, key,
                                            value, null);
                                    break;
                                }
                            }
                        }
                        //表示已經轉化成紅黑樹類型了
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            //調用putTreeVal方法,將該元素添加到樹中去
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                    value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    //當在同一個節點的數目達到8個的時候,則擴張數組或將給節點的數據轉爲tree
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        //計數
        addCount(1L, binCount);
        return null;
    }
總結:

在學習 Map 的時候,建議先對 數組、鏈表、紅黑樹 有一定的瞭解,然後搞懂其數據結構,先對HashMap進行掌握與熟悉,HashTable 和 ConcurrentHashMap 可以看做是 HashMap 的變形,更好的去學習。源碼還是要去一點點的閱讀,瞭解其設計理念,則在以後的面試中會百戰不殆。

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