這一篇着重介紹HasHMap中的一些經常使用方法:數組
put()性能優化
get()架構
**resize()**併發
首先介紹resize()這個方法,在我看來這是HashMap中一個很是重要的方法,是用來調整HashMap中table的容量的,在不少操做中多須要從新計算容量。分佈式
源碼以下:微服務
1 final Node<K,V>[] resize() { 2 Node<K,V>[] oldTab = table; 3 int oldCap = (oldTab == null) ? 0 : oldTab.length; 4 int oldThr = threshold; 5 int newCap, newThr = 0; 6 if (oldCap > 0) { 7 if (oldCap >= MAXIMUM_CAPACITY) { 8 threshold = Integer.MAX_VALUE; 9 return oldTab; 10 } 11 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 12 oldCap >= DEFAULT_INITIAL_CAPACITY) 13 newThr = oldThr << 1; // double threshold 14 } 15 else if (oldThr > 0) // initial capacity was placed in threshold 16 newCap = oldThr; 17 else { // zero initial threshold signifies using defaults 18 newCap = DEFAULT_INITIAL_CAPACITY; 19 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 20 } 21 if (newThr == 0) { 22 float ft = (float)newCap * loadFactor; 23 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? 24 (int)ft : Integer.MAX_VALUE); 25 } 26 threshold = newThr; 27 @SuppressWarnings({"rawtypes","unchecked"}) 28 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 29 table = newTab; 30 if (oldTab != null) { 31 for (int j = 0; j < oldCap; ++j) { 32 Node<K,V> e; 33 if ((e = oldTab[j]) != null) { 34 oldTab[j] = null; 35 if (e.next == null) 36 newTab[e.hash & (newCap - 1)] = e; 37 else if (e instanceof TreeNode) 38 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 39 else { // preserve order 40 Node<K,V> loHead = null, loTail = null; 41 Node<K,V> hiHead = null, hiTail = null; 42 Node<K,V> next; 43 do { 44 next = e.next; 45 if ((e.hash & oldCap) == 0) { 46 if (loTail == null) 47 loHead = e; 48 else 49 loTail.next = e; 50 loTail = e; 51 } 52 else { 53 if (hiTail == null) 54 hiHead = e; 55 else 56 hiTail.next = e; 57 hiTail = e; 58 } 59 } while ((e = next) != null); 60 if (loTail != null) { 61 loTail.next = null; 62 newTab[j] = loHead; 63 } 64 if (hiTail != null) { 65 hiTail.next = null; 66 newTab[j + oldCap] = hiHead; 67 } 68 } 69 } 70 } 71 } 72 return newTab; 73 }
能夠看到這段代碼很是龐大,其內容能夠分爲兩大部分:高併發
第一部分計算並生成新的哈希表(空表):源碼分析
1 // 記錄原表 2 Node<K,V>[] oldTab = table; 3 // 獲得原來哈希表的總長度,及原來總容量 4 int oldCap = (oldTab == null) ? 0 : oldTab.length; 5 // 獲得原來最佳容量 6 int oldThr = threshold; 7 // 存放新的總容量、新最佳容量的變量 8 int newCap, newThr = 0; 9 if (oldCap > 0) { 10 // 原來總容量達到或超過HashMap的最大容量,則最佳容量設置爲int類型的最大值 11 // 且原來容量不變,直接返回,不作後需調整 12 if (oldCap >= MAXIMUM_CAPACITY) { 13 threshold = Integer.MAX_VALUE; 14 return oldTab; 15 } 16 // 讓新的總容量等於原來容量的二倍 17 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 18 oldCap >= DEFAULT_INITIAL_CAPACITY) 19 // 新的最佳容量也變爲原來的二倍 20 newThr = oldThr << 1; 21 } 22 // 原來總容量爲0,將新的總容量設置爲最佳容量,構造方法出入參數是一個派生的Map的時候,就會使用派生的Map計算出新的最佳容量 23 else if (oldThr > 0) 24 newCap = oldThr; 25 else { 26 // 原來總容量和原來最佳容量都沒有定義 27 // 新的總容量設爲默認值16 28 // 新的最佳容量=默認負載因子×默認容量=0.75×16=12 29 newCap = DEFAULT_INITIAL_CAPACITY; 30 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 31 } 32 // 判斷上述操做後新的最佳容量是否計算,若沒有,就利用負載因子和新的總容量計算 33 if (newThr == 0) { 34 float ft = (float)newCap * loadFactor; 35 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? 36 (int)ft : Integer.MAX_VALUE); 37 } 38 // 更新當前的最佳容量 39 threshold = newThr; 40 @SuppressWarnings({"rawtypes","unchecked"}) 41 // 生成新的哈希表,即一維數組 42 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 43 // 更新哈希表 44 table = newTab;
能夠看出上述操做僅僅是生成了一張大小合適的哈希表,但表仍是空的,後面的操做就是把之前的表中的元素從新排列,移動到當前表中合適的位置!性能
第二部分將原表元素移動到新表合適的位置:學習
1 // 先判斷原表是或否爲空 2 if (oldTab != null) { 3 // 遍歷原表(一維數組)中的全部元素, 4 for (int j = 0; j < oldCap; ++j) { 5 // 記錄原來一維數組中下標爲j的元素 6 Node<K,V> e; 7 // 只對有效元素進行操做 8 if ((e = oldTab[j]) != null) { 9 //將原表中的元素置空 10 oldTab[j] = null; 11 if (e.next == null) 12 // 當前元素沒有後繼,那麼直接把它放在新表中合適位置 13 // 其中e.hash & (newCap - 1)在我上一篇博客有介紹 14 // 就是以該節點的hash值和新表總容量取餘,將餘數做爲下標 15 newTab[e.hash & (newCap - 1)] = e; 16 else if (e instanceof TreeNode) 17 // 當前元素有後繼,且後繼是紅黑樹 18 // 進行有關紅黑樹的相應操做 19 // 這裏不詳細介紹紅黑樹的操做 20 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 21 else { 22 // 這裏就進行有關鏈表的移動 23 // 這兩組結點變量,分別表明兩條不一樣鏈表的頭和尾 24 // 低位的頭和尾 25 Node<K,V> loHead = null, loTail = null; 26 // 高位的頭和尾 27 Node<K,V> hiHead = null, hiTail = null; 28 // 下一節點 29 Node<K,V> next; 30 do { 31 // 讓next等於當前結點的後繼結點 32 next = e.next; 33 // 這個位運算實際上判斷的是該節點在新表中的位置是否發生改變 34 // 成立則說明沒有改變,仍是原來表中下標爲j的位置 35 if ((e.hash & oldCap) == 0) { 36 // 如果首結點,則讓低位的頭等於當前結點 37 if (loTail == null) 38 loHead = e; 39 else 40 // 若不是首結點,則讓低位的尾等於當前結點 41 loTail.next = e; 42 // 讓低位的尾移動到當前 43 loTail = e; 44 } 45 // 這裏就說明其在新表中的位置發生了改變,則要將其放入另外一條鏈表 46 else { 47 // 如果首結點,則讓高位的頭等於當前結點 48 if (hiTail == null) 49 hiHead = e; 50 else 51 // 若不是首結點,則讓高位的尾等於當前結點 52 hiTail.next = e; 53 // 讓高位的尾移動到當前 54 hiTail = e; 55 } 56 } while ((e = next) != null); 57 // 原來位置的這條鏈表還存在 58 if (loTail != null) { 59 // 置空低位的尾的next 60 loTail.next = null; 61 // 將該鏈表的頭結點放入新表下標爲j的位置,即原表中的原位置 62 newTab[j] = loHead; 63 } 64 // 新位置上的鏈表存在 65 if (hiTail != null) { 66 // 置空高位的尾的next 67 hiTail.next = null; 68 // 將該鏈表的頭結點放入新表中下標爲j+原表長度的位置 69 newTab[j + oldCap] = hiHead; 70 } 71 } 72 } 73 } 74 } 75 return newTab;
鏈表的移動如圖:
能夠看出,這個方法可使得單個結點從新散列,鏈表能夠拆分紅兩條,紅黑樹從新移動,這樣使得新的哈希表分佈比之前均勻!
下面來分析put方法:
源碼以下:
1 public V put(K key, V value) { 2 return putVal(hash(key), key, value, false, true); 3 }
這裏咱們能夠知道其調用了內部的一個putVal方法:
首先第一個參數是經過內部的hash方法(在前一篇博客有介紹過)計算出鍵對象的hash(int類型)值,再把key和value對象傳過去,置於後面兩個參數先不着急
先來看下putVal方法是如何說明的:
1 /** 2 * Implements Map.put and related methods 3 * 4 * @param hash hash for key 5 * @param key the key 6 * @param value the value to put 7 * // 看以看出,put方法傳入的onlyIfAbsent是false,那麼就會改變原來已存在的值 8 * @param onlyIfAbsent if true, don't change existing value 9 * // 這個參數先不考慮,日後慢慢分析 10 * @param evict if false, the table is in creation mode. 11 * @return previous value, or null if none 12 */ 13 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
該方法內容:
1 // 用於保存原表 2 Node<K,V>[] tab; 3 // 保存下標爲hash的結點 4 Node<K,V> p; 5 // n用來記錄表長 6 int n, i; 7 // 先檢查原表是否存在,或者是空表 8 if ((tab = table) == null || (n = tab.length) == 0) 9 // 若是爲空就生成一張大小爲16的新表 10 n = (tab = resize()).length; 11 if ((p = tab[i = (n - 1) & hash]) == null) 12 // 若是以該方法形參hash對錶長取餘,令其做爲下標的表中的元素爲空,那麼就產生一個新結點放在這個位置 13 tab[i] = newNode(hash, key, value, null); 14 else { 15 // 若是該結點不空,那麼就會出現兩種狀況:鏈表和紅黑樹 16 Node<K,V> e; K k; 17 if (p.hash == hash && 18 ((k = p.key) == key || (key != null && key.equals(k)))) 19 // 若是當前結點的hash而且key值(指針值和內容值)相等,因爲onlyIfAbsent是false,那麼就會改變這個結點的V值,先用e將其保存起來 20 e = p; 21 else if (p instanceof TreeNode) 22 // 若是當前結點是一棵紅黑樹,那麼就進行紅黑樹的平衡,這裏不討論紅黑樹的問題 23 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 24 else { 25 // 這裏就對鏈表進行操做 26 // 從頭開始遍歷這條鏈表 27 for (int binCount = 0; ; ++binCount) { 28 if ((e = p.next) == null) { 29 // 若是該節點的next爲空 30 // 就須要新增一個結點追加其後 31 p.next = newNode(hash, key, value, null); 32 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 33 // 這裏進行紅黑樹閾值的判斷,因爲TREEIFY_THRESHOLD默認值是8,binCount是從0開始,那麼當鏈表長度大於等於8的時候,就將該鏈表轉換成紅黑樹,而且結束循環 34 treeifyBin(tab, hash); 35 break; 36 } 37 // 這裏和以前的判斷是同樣的 38 if (e.hash == hash && 39 ((k = e.key) == key || (key != null && key.equals(k)))) 40 break; 41 // 讓p = p->next 42 p = e; 43 } 44 } 45 // 若e非空,則就是說明原表中存在hash值相等,且key的值或內容相同的結點 46 if (e != null) { 47 // 將原來的V值保存 48 V oldValue = e.value; 49 // 判斷是不是須要進行覆蓋原來V值的操做 50 if (!onlyIfAbsent || oldValue == null) 51 // 覆蓋原來的V值 52 e.value = value; 53 // 這個方法是一個空的方法,預留的一個操做,不用去管它 54 afterNodeAccess(e); 55 // 因爲在這裏面的操做只是替換了原來的V值,並無改變原來表的大小,直接返回oldValue 56 return oldValue; 57 } 58 } 59 // 操做數自增 60 ++modCount; 61 // 實際大小自增 62 // 若其大於最佳容量進行擴容的操做,使其分佈均勻 63 if (++size > threshold) 64 resize(); 65 // 這也是一個空的方法,預留操做 66 afterNodeInsertion(evict); 67 // 並無替換原來的V值,返回null 68 return null;
下來是get方法,邏輯相對簡單不難分析:
在此我向你們推薦一個架構學習交流羣。交流學習羣號:821169538 裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多。
1 public V get(Object key) { 2 Node<K,V> e; 3 return (e = getNode(hash(key), key)) == null ? null : e.value; 4 }
一樣也是經過hash方法計算出key對象的hash值,調用內部的getNode方法:
1 final Node<K,V> getNode(int hash, Object key) { 2 // 記錄表對象 3 Node<K,V>[] tab; 4 // 記錄第一個結點和當前節點 5 Node<K,V> first, e; 6 // 記錄表長 7 int n; 8 // 記錄K值 9 K k; 10 // 表非空或者長度大於0纔對其操做 11 // 而且key的hash值對錶長取餘爲下標,其所對應的哈希表中的結點存在 12 if ((tab = table) != null && (n = tab.length) > 0 && 13 (first = tab[(n - 1) & hash]) != null) { 14 // 當前結點知足狀況,直接返回給該節點 15 if (first.hash == hash && 16 ((k = first.key) == key || (key != null && key.equals(k)))) 17 return first; 18 // 後面就分爲兩種狀況:在紅黑樹或者鏈表中查找 19 if ((e = first.next) != null) { 20 // 當前結點是紅黑樹,進行紅黑樹的查找 21 if (first instanceof TreeNode) 22 return ((TreeNode<K,V>)first).getTreeNode(hash, key); 23 // 進行鏈表的遍歷 24 do { 25 if (e.hash == hash && 26 ((k = e.key) == key || (key != null && key.equals(k)))) 27 return e; 28 } while ((e = e.next) != null); 29 } 30 } 31 return null; 32 }