JDK1.8 不一樣的HashMap
前言
HashMap想必大家都很熟悉JDK1.8 的 HashMap 隨便一搜都是一大片一大片的那為什麼還要寫呢我會把它精簡一下一方面有利於自己的學習另一方面希望讓大家更好理解核心內容。本篇主要講解HashMap原始碼的主要流程。本篇借鑑了 我們一起來看下JDK1.8做了哪些最佳化~
JDK1.7 VS JDK1.8 原始碼比較
最佳化概述之後會一一細說
resize 擴容最佳化
引入了紅黑樹目的是避免單條連結串列過長而影響查詢效率紅黑樹演算法請參考 http://blog.csdn.net/v_july_v/article/details/6105630 HashMap整體結構如圖
解決了多執行緒死迴圈問題但仍是非執行緒安全的多執行緒時可能會造成資料丟失問題。
JDK1.7 VS JDK1.8 效能比較
Hash較均勻的情況
Hash不均勻的情況
JDK1.8 中的 HashMap 是不是666的飛起效能碾壓JDK1.7中的HashMap~ 但是原始碼可比JDK1.7難讀一些了接下來一起來學習下 HashMap 的原始碼提前透露下核心方法是 resize
和 putVal
。
預備知識
HashMap 中
table
角標計算及table.length
始終為2的冪即 2 ^ n對應的程式碼是
/** * Returns a power of two size for the given target capacity. */ 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 = MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; } // 取key的hashCode值、高位運算、取模運算 // 在JDK1.8的實現中最佳化了高位運算的演算法 // 透過hashCode()的高16位異或低16位實現的(h = k.hashCode()) ^ (h >>> 16) // 主要是從速度、功效、質量來考慮的這麼做可以在陣列table的length比較小的時候 // 也能保證考慮到高低Bit都參與到Hash的計算中同時不會有太大的開銷。 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
我們在程式碼中經常會看到這樣計算table索引
這就是 table.length
為何是 2 ^ n 的原因了圖中 n 為 table的長度
這樣計算之後 在 n 為 2 ^ n 時 其實相當於 hash % n& 當然比 % 效率高這也是HashMap 計算角標時的巧妙之處。
capacity、threshold和loadFactor之間的關係
capacity table的容量預設容量是16
threshold table擴容的臨界值
loadFactor 負載因子一般 threshold = capacity * loadFactor預設的負載因子0.75是對空間和時間效率的一個平衡選擇建議大家不要修改。
基本元素(原 Entity)
static class Nodeimplements Map.Entry { final int hash; // node的hash值 final K key; // node的key V value; // node的value Node next; // node指向下一個node的引用 Node(int hash, K key, V value, Node 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; } }
resize()方法
我們將resize()
方法分為兩部分第一部分是生成newTable的過程第二部分是遷移資料。
final Node[] resize() { Node [] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap = DEFAULT_INITIAL_CAPACITY) newThr = oldThr 0) // initial capacity was placed in threshold newCap = oldThr; 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 [] newTab = (Node [])new Node[newCap]; table = newTab; ... return newTab; }
以上程式碼展示了 newTable 的建立過程由於 table、capacity、threshold等是懶載入所以會有一系列的判斷及對應的初始化這些不是特別重要重點在下邊註釋標在程式碼塊上紅黑樹較為複雜這裡不做講解後續會考慮單講紅黑樹
if (oldTab != null) { for (int j = 0; j e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) // 如果只有table[j]中有元素 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) // 如果e是紅黑樹節點走紅黑樹替換方式 ((TreeNode)e).split(this, newTab, j, oldCap); else { // 如果 table[j] 後是一個連結串列 將原連結串列拆分為兩條鏈分別放到newTab中 Node loHead = null, loTail = null; Node hiHead = null, hiTail = null; Node next; 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; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } }
重點就在這個 連結串列拆分
首次看到 e.hash & oldCap
我是懵逼的。。。其實這樣是一個取巧的辦法效能上優於rehash
的過程我們用圖解方式去解釋如何進行連結串列拆分
(a) 是未擴容時 key1
和 key2
得出的 hash & (n - 1) 均為 5。
(b) 是擴容之後key1
計算出的 newTab 角標依舊為 5但是 key2
由於 擴容 得出的角標 加了 16即21 16是oldTab的length再來看e.hash & oldCap
oldCap.length即n 本身為 0000 0000 0000 0000 0000 0000 0001 0000
這個位與運算可以得出擴容後哪些key 在 擴容新增位時1哪些是0一個位運算替換了rehash
過程是不是得給100個贊~大概擴容的過程如下
執行緒安全問題
JDK1.7 HashMap在多執行緒的擴容時確實會出現迴圈引用導致下次get時死迴圈的問題具體可以參考。很多文章在說到死迴圈時都以JDK1.7來舉例其實JDK1.8的最佳化已經避免了死迴圈這個問題但是會造成資料丟失問題下面我舉個例子需要對應上邊resize的下半部分程式碼
建立 thread1 和 thread2 去新增資料此時都在resize兩個執行緒分別建立了兩個newTable並且thread1在table = newTab;
處排程到thread2(沒有給table賦值)等待thread2擴容之後再排程回thread1注意擴容時oldTab[j] = null;
也就將 oldTable中都清掉了當回到thread1時將table指向thread1的newTable但訪問oldTable中的元素全部為null所以造成了資料丟失。
putVal()方法
put
方法其實呼叫了putVal
引數onlyIfAbsent
表示如果為true若put的位置已經有value則不修改putIfAbsent
方法中傳true
這個方法的重點在於 TREEIFY_THRESHOLD
這個變數如果連結串列長度 >= TREEIFY_THRESHOLD - 1
則呼叫 treeifyBin
方法 從它的註釋上可以看出這個方法會把這條鏈所有的Node變為紅黑樹結構。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node[] tab; Node p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode )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; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }/** * Replaces all linked nodes in bin at index for given hash unless * table is too small, in which case resizes instead. */final void treeifyBin(Node [] tab, int hash) { int n, index; Node e; if (tab == null || (n = tab.length) hd = null, tl = null; do { TreeNode p = replacementTreeNode(e, null); // Node 替換為 TreeNode 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); // 紅黑樹轉換過程 } }
entrySet()遍歷
我們在遍歷HashMap的時候都會使用 map.entrySet().iterator()看下這個 iterator
是什麼
public Set> entrySet() { Set > es; return (es = entrySet) == null ? (entrySet = new EntrySet()) : es; } final class EntrySet extends AbstractSet > { ... public final Iterator > iterator() { return new EntryIterator(); } ... } final class EntryIterator extends HashIterator implements Iterator > { public final Map.Entry next() { return nextNode(); } } abstract class HashIterator { Node next; // next entry to return Node current; // current entry int expectedModCount; // for fast-fail int index; // current slot HashIterator() { expectedModCount = modCount; Node [] t = table; current = next = null; index = 0; if (t != null && size > 0) { // advance to first entry do {} while (index nextNode() { Node [] t; Node 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 p = current; if (p == null) throw new IllegalStateException(); if (modCount != expectedModCount) throw new ConcurrentModificationException(); current = null; K key = p.key; removeNode(hash(key), key, null, false, false); expectedModCount = modCount; } }
跟 ArrayList
、LinkedList
一樣還是modCount
和expectedModCount
的問題expectedModCount
是iterator
構造時賦的值等於當時的modCount
所以如果已經生成了iterator
如果擅自使用map.put()
等操作會使modCount
變化導致expectedModCount != modCount
會丟擲ConcurrentModificationException
。
結尾
好了以上除了紅黑樹HashMap中的我認為的核心內容至此就說完了可以看出JDK一直在許多細節上不斷地在做最佳化作為我們還是需要不斷地修煉去發現這些程式碼中的驚豔之處
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2459/viewspace-2812739/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Java集合——HashMap(jdk1.8)JavaHashMapJDK
- Jdk1.8下的HashMap原始碼分析JDKHashMap原始碼
- JDK1.8 hashMap原始碼分析JDKHashMap原始碼
- HashMap原始碼分析 JDK1.8HashMap原始碼JDK
- JDK1.8原始碼分析之HashMapJDK原始碼HashMap
- JDK1.8原始碼分析筆記-HashMapJDK原始碼筆記HashMap
- Java HashMap 原始碼逐行解析(JDK1.8)JavaHashMap原始碼JDK
- HashMap原始碼(JDK1.8)-手動註釋HashMap原始碼JDK
- 集合框架原始碼學習之HashMap(JDK1.8)框架原始碼HashMapJDK
- 小白也能看懂的JDK1.8前_HashMap的擴容機制原理JDKHashMap
- Steam使用者的“不一樣”
- SVM之不一樣的視角
- 不一樣的角度理解Vue元件Vue元件
- 不一樣的Flink入門教程
- 原始碼分析系列1:HashMap原始碼分析(基於JDK1.8)原始碼HashMapJDK
- 不一樣的釋出會不一般的品高雲
- HashMap解析(主要JDK1.8,附帶1.7出現的問題以及區別)HashMapJDK
- str跟unicode不一樣Unicode
- 2019 總結不一樣!
- 不一樣的HTTP快取體驗HTTP快取
- 不一樣的django2.0筆記Django筆記
- 不一樣的 Android 堆疊抓取方案Android
- 不一樣的圖片載入方式
- “每天不一樣”的武漢,遇上“雲”又會怎樣?
- Android Rxjava:圖解不一樣的詮釋AndroidRxJava圖解
- win下面不一樣的git bush體驗Git
- 人大:和清、北做不一樣的AIAI
- 不一樣的命令模式(設計模式十五)設計模式
- 不一樣的 SQL Server 日期格式化SQLServer
- 海外的bug-hunters,不一樣的403bypass
- windows與linux ping 顯示的ip不一樣WindowsLinux
- Apk 極限壓縮(說點不一樣的)APK
- 什麼是EOS(不一樣的角度看柚子)
- Flutter - 不一樣的跨平臺解決方案Flutter
- 不一樣的工廠模式(設計模式六)設計模式
- MySQL8,不一樣的安裝體驗MySql
- 同樣是黑客少年,但他們可能有不一樣的命運黑客
- 【Java】JDK1.8之前HashMap併發情況為什麼會發生死迴圈JavaJDKHashMap