1. 程式人生 > >JDK原始碼分析-HashMap

JDK原始碼分析-HashMap

一.HashMap的內部屬性

1.1 成員變數

1.1.1 size:

HashMap包含的KV鍵值對的數量,也就是我們通常呼叫Map.size()方法的返回值

    public int size() {
        return size;
    }
1.1.2 modCount

HashMap的結構被修改的次數(包括KV對映數量和內部結構rehash次數),用於判斷迭代器梳理中不一致的快速失敗。

abstract class HashIterator {
...
  final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }
...
}
1.1.3 threshold

下一次擴容時的閾值,達到閾值便會觸發擴容機制resize(閾值 threshold = 容器容量 capacity * 負載因子 load factor)。也就是說,在容器定義好容量之後,負載因子越大,所能容納的鍵值對元素個數就越多。計算方法如下:

 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;
    }
1.1.4 loadFactor

負載因子,預設是0.75

1.1.5 Node<K,V>[] table

底層陣列,充當雜湊表的作用,用於儲存對應hash位置的元素,陣列長度總是2的N次冪

1.2 內部類

1.2.1 Node<K,V>
/**
     * 定義HashMap儲存元素結點的底層實現
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//元素的雜湊值 由final修飾可知,當hash的值確定後,就不能再修改
        final K key;// 鍵,由final修飾可知,當key的值確定後,就不能再修改
        V value; // 值
        Node<K,V> next; // 記錄下一個元素結點(單鏈表結構,用於解決hash衝突)

        
        /**
         * Node結點構造方法
         */
        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; }

        /**
         * 為Node重寫hashCode方法,值為:key的hashCode 異或 value的hashCode 
         * 運算作用就是將2個hashCode的二進位制中,同一位置相同的值為0,不同的為1。
         */
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        /**
         * 修改某一元素的值
         */
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        /**
         * 為Node重寫equals方法
         */
        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;
        }
    }
1.2.2 TreeNode<K,V>
 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        //與left、right聯合使用實現樹結構   
        TreeNode<K,V> parent;  
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        // needed to unlink next upon deletion
        TreeNode<K,V> prev;   
        //記錄樹節點顏色 
        boolean red;

     /**
     * 操作方法
     * 包括:樹化、鏈棧化、增刪查節點、根節點變更、樹旋轉、插入/刪除節點後平衡紅黑樹
     */
     ...
}

1.3 Key的hash演算法

Key的hash演算法原始碼如下:

  static final int hash(Object key) {
        int h;
         ///key.hashCode()為雜湊演算法,返回初始雜湊值
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    } 

因為HashMap中是允許key 為null的鍵值對,所以先判斷了key == null。當key 不為null的時候,hash演算法是先通過key.hashCode()計算出一個hash值再與改hash值的高16位做異或運算(有關異或運算請移步:java運算子 與(&)、非(~)、或(|)、異或(^)) 上面的key.hashCode()已經計算出來了一個hash雜湊值,可以直接拿來用了,為何還要做一個異或運算? 是為了對key的hashCode進行擾動計算(),防止不同hashCode的高位不同但低位相同導致的hash衝突。簡單點說,就是為了把高位的特徵和低位的特徵組合起來,降低雜湊衝突的概率,也就是說,儘量做到任何一位的變化都能對最終得到的結果產生影響

二. HashMap的初始化

HashMap的初始化有以下四種方法:

  1. HashMap()
  2. HashMap(int initialCapacity)
  3. HashMap(int initialCapacity, float loadFactor)
  4. HashMap(Map<? extends K, ? extends V> m)

方法1的原始碼如下:

   public HashMap() {
        //使用預設的DEFAULT_LOAD_FACTOR = 0.75f
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

  其中的方法2本質上都是呼叫了方法3。initialCapacity是初始化HashMap的容量,loadFactor是在1.1.4中提到的負載因子。 方法3的原始碼註釋如下:

 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);
    } 

方法4原始碼註釋如下:

public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

    /**
     * Implements Map.putAll and Map constructor
     *
     * @param m 要初始化的map
     * @param evict 初始化構造map時為false,其他情況為true
     */
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        //判斷當前m容量
        if (s > 0) {
            // 初始化
            if (table == null) { 
                //ft按照預設載入因子計算ft=s/0.75 +1計算出來
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold)
                //s大於threshlod,需要擴容
                resize();
            //遍歷m,並通過putVal初始化資料
            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);
            }
        }
    }

  

三. put過程

3.1 put的正常呼叫過程

put方法是HashMap的增加KV對的入口,putVal方法是具體實現,整個過程的大致流程如下:

  1. 對key的hashCode()做hash,然後再計算index;
  2. 如果沒碰撞直接放到bucket裡;
  3. 如果碰撞了,以連結串列的形式存在buckets後;
  4. 如果碰撞導致連結串列過長(大於等於TREEIFY_THRESHOLD),就把連結串列轉換成紅黑樹;
  5. 如果節點已經存在就替換old value(保證key的唯一性)
  6. 如果bucket滿了(超過load factor*current capacity),就要resize
   public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    } 

3.2 put過程剖析

putVal方法的原始碼解析如下:

/**
     * Implements Map.put and related methods
     *
     * @param hash key的hash值
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent 為true不修改已經存在的值
     * @param evict 為false表示建立
     * @return previous value, or null if none
     */
    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;
        //根據hash值計算出index,並校驗當前tab中index的值是否存在
        if ((p = tab[i = (n - 1) & hash]) == null)
            //當前tab中index的值為空,則直接插入到tab中
            tab[i] = newNode(hash, key, value, null);
        else {
            //當前tab節點已經存在hash相同的值
            Node<K,V> e; K k;
            //分別比較hash值和key值相等,就直接替換現有的節點
            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;
        //判斷是否需要resize擴容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

  

 

 

四. 擴容

4.1 什麼條件下會擴容

當向容器新增元素的時候,會判斷當前容器的元素個數,如果大於等於threshold閾值(即當前陣列的長度乘以載入因子的值的時候),就要自動擴容了。

4.2 如何擴容

HashMap的擴容是呼叫了resize方法(初始化的時候也會呼叫),擴容是按照兩倍的大小進行的,原始碼如下:

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        //取出tabble的大小
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //當map不為空的時候
        if (oldCap > 0) {
            //map已經大於最大MAXIMUM_CAPACITY = 1 << 30
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //向左位移1,擴大兩倍
                newThr = oldThr << 1; // double threshold
        }
        //也就是HashMap初始化是呼叫了HashMap(initialCapacity)或者HashMap(initialCapacity,loadFactor)構造方法
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        //使用的是HashMap()構造方法
        else {               // zero initial threshold signifies using defaults
            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;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            //當map不為空,需要賦值原有map中的資料到新table中
            ...
        }
        return newTab;
    }

  

從原始碼中可以看出,resize擴容是一個非常消耗效能的操作,所以在我們可以預知HashMap大小的情況下,預設的大小能夠避免resize,也就能有效的提高HashMap的效能。

五. 樹化與連結串列化

5.1 什麼條件下會樹化

當binCount達到閾值TREEIFY_THRESHOLD - 1的時候就會發生樹化(TREEIFY_THRESHOLD = 8),也就是binCount>=7的時候就會進入到treeifyBin方法,但只有當大於MIN_TREEIFY_CAPACITY(= 64)才會觸發treeify樹化

 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
      treeifyBin(tab, hash);

5.2 樹化演算法

演算法

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        // 通過hash求出bucket的位置    
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                // 將Node節點包裝成TreeNode
                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)
                // 對TreeNode連結串列進行樹化
                hd.treeify(tab);
        }
    }

        final void treeify(Node<K,V>[] tab) {
            TreeNode<K,V> root = null;
            //遍歷TreeNode
            for (TreeNode<K,V> x = this, next; x != null; x = next) {
                //next向前
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                //當根節點為空,就賦值
                if (root == null) {
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                else {
                   //root存在,就自頂向下遍歷
                    ...
                  
            }
            moveRootToFront(tab, root);
        } 

六. get過程

get方法相對於put要簡單一些,原始碼如下:

public V get(Object key) {
        Node<K,V> e;
        //根據key取hash,演算法與put中一樣
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //1. 判斷table不為空
        //2. table長度大於0
        //3. 與put方法一樣計算tab的索引,並判斷是否為空
        if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
            //比較第一個節點的hash和key是都都相等
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                //紅黑樹:直接呼叫getTreeNode()
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
		    //連結串列:通過.next() 迴圈獲取
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

  

 

 

六. 常見問題

5.1 併發常見下CPU100%問題

Hash並非是執行緒安全的,在併發場景下,錯誤的使用HashMap可能會出現CPU100%的問題 曾今有人在JDK1.4版本中的HashMap中提出過這樣一個bug,官方也給出了答覆“並非java或jvm的bug,而是使用不當”,當時所提出的地址是:JDK-6423457 : (coll) High cpu usage in HashMap.get() 左耳朵耗子前輩也做過分享:疫苗:JAVA HASHMAP的死迴圈

5.2 ConcurrentModificationException

https://blog.csdn.net/u010527630/article/details/69917063

 

 

&n