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的初始化有以下四種方法:
- HashMap()
- HashMap(int initialCapacity)
- HashMap(int initialCapacity, float loadFactor)
- 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方法是具體實現,整個過程的大致流程如下:
- 對key的hashCode()做hash,然後再計算index;
- 如果沒碰撞直接放到bucket裡;
- 如果碰撞了,以連結串列的形式存在buckets後;
- 如果碰撞導致連結串列過長(大於等於TREEIFY_THRESHOLD),就把連結串列轉換成紅黑樹;
- 如果節點已經存在就替換old value(保證key的唯一性)
- 如果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