在上篇文章中咱們大體介紹了HashMap原理,本文主要圍繞Java8HashMap作了哪些優化.html
在上文提到jdk1.7中HashMap採用數組+鏈表實現,雖然使用鏈表處理衝突,同一hash值的元素都存儲在一個鏈表中,但當同一鏈表上的元素較多又想要查詢最早插入的元素時,經過key依次尋找顯然效率較低.因此java8HashMap採用數組+鏈表+紅黑樹方式實現,當鏈表長度超過閾值8時,會將鏈表轉換爲紅黑樹.java
/**
* 鏈表最大長度,若超過8,桶中鏈表轉成紅黑樹
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 桶中結點小於該長度,紅黑樹轉成鏈表.中間有2個緩衝值的緣由是避免頻繁的切換浪費計算機資源
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 哈希表的最小樹形化容量,只有鍵值對數量大於64纔會發生轉換,將鏈式結構轉化成樹型結構,
* 不然採用擴容來避免衝突,至少4*TREEIFY_THRESHOLD來避免擴容和樹形結構之間的衝突
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 相對於java7,8用node替換了Entry類,它們的結構大致相同.一個顯著的區別
* node有派生類TreeNode,經過這種繼承關係,鏈表很容易轉換成樹
*/
static class Node implements Map.Entry {
final int hash;
final K key;
V value;
Node next;
Node(int hash, K key, V value, Node next) {...}
public final K getKey() {...}
public final V getValue() {...}
public final String toString() {...}
public final int hashCode() {...}
public final V setValue(V newValue) {...}
public final boolean equals(Object o) {...}
}
static final class TreeNode extends LinkedHashMap.Entry {
TreeNode parent; //父結點
TreeNode left; //結點的左孩子
TreeNode right; //結點的右孩子
TreeNode prev; //前一個元素結點
boolean red; //true表示紅結點,false表示黑結點
TreeNode(int hash, K key, V val, Node next) {...}
final TreeNode root() {...}
static void moveRootToFront(Node[] tab, TreeNode root) {...}
final TreeNode find(int h, Object k, Class kc) {...}
final TreeNode getTreeNode(int h, Object k) {...}
static int tieBreakOrder(Object a, Object b) {...}
final void treeify(Node[] tab) {...}
final Node untreeify(HashMap map) {...}
final TreeNode putTreeVal(HashMap map, Node[] tab,
int h, K k, V v) {...}
final void removeTreeNode(HashMap map, Node[] tab,
boolean movable) {...}
final void split(HashMap map, Node[] tab, int index, int bit) {...}
/* ------------------------------------------------------------ */
// Red-black tree methods, all adapted from CLR
static TreeNode rotateLeft(TreeNode root,
TreeNode p) {...}
static TreeNode rotateRight(TreeNode root,
TreeNode p) {...}
static TreeNode balanceInsertion(TreeNode root,
TreeNode x) {...}
static TreeNode balanceDeletion(TreeNode root,
TreeNode x) {...}
static boolean checkInvariants(TreeNode t) {...}
}
複製代碼
TreeNode繼承關係圖:
node
put操做進行以下步驟:
①.經過hash算法計算key的hash值 ②.判斷哈希表是否爲空或爲null,爲空或爲null時調用resize方法進行擴容
③.根據key計算hash值獲得桶索引,若沒有碰撞即桶中無結點直接添加
④.若發生碰撞,判斷該桶中首個元素是否與key同樣,若相同記錄該結點
⑤.若不一樣,判斷該桶結構是不是紅黑樹,如果在樹中插入鍵值對
⑥.若不是紅黑樹,即爲鏈表.遍歷鏈表,判斷鏈表長度是否大於8,若大於8將鏈表轉成紅黑樹,在紅黑樹中插入鍵值對;
⑦.若未超過8且找到key相同的結點記錄此結點,若沒有找到則尾插結點
⑧.若記錄的結點不爲null且onlyIfAbsent爲false或舊值爲null進行替換返回舊值,不然不能替換
⑨.插入成功後size+1,校驗是否超過閾值threshold,若過載則擴容算法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* @param hash key通過hash計算後的hash值
* @param key 鍵
* @param value 值
* @param onlyIfAbsent 若爲true,不會替換value
* @param evict 若爲false,哈希表在建立模式中
* @return 返回被替換值.返回null可能被替換的就是null,或不存在key鍵對象
* 沒有進行替換操做
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
// 若哈希表爲空或爲null,調用resize方法建立一個
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;
// 若桶中第一個結點的hash值相同而且equals方法返回true時進行替換
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) {
//若鏈表中無相應key進行尾插
p.next = newNode(hash, key, value, null);
// 鏈表長度大於8,將其結構換成紅黑樹
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;
}
}
// 若結點不爲null,將值進行替換,返回舊值
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;
}
複製代碼
java7HashMap的put操做與java8有以下區別:
①.java7HashMap採用鏈表處理衝突hash算法對key的hash值作了4擾動,而java8引入了紅黑樹來處理較多的哈希衝突,遍歷的時間複雜度由O(n)→O(logn)因此其簡化了他hash算法將hash值的高位與低位進行混合
②.java7桶中僅多是鏈式結構,而java8還多是紅黑樹,因此java8只能先查看桶中首個元素後須要判斷桶的數據結構根據其結構採用不一樣的方式處理
③.java7新增結點先判斷是否超過閾值threshold再添加,java8先添加後判斷
④.java7採用頭插,而java8若桶是鏈式結構採用尾插
數組
當桶中鏈表長度超過8時,會調用此方法鏈表結點轉紅黑樹結點進行以下操做:
①.判斷其是否符合樹形化條件,若不符合進行擴容解決更多衝突
②.若符合遍歷桶中鏈表全部結點,將頭結點設爲紅黑樹的根結點,建立與鏈表結點內容一致的樹形結點安全
final void treeifyBin(Node[] tab, int hash) {
int n, index; Node e;
//若當前哈希表爲空或長度小於最小化樹形容量進行擴容來解決更多衝突
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//當前位置桶不爲null
else if ((e = tab[index = (n - 1) & hash]) != null) {
//紅黑樹頭結點,尾結點
TreeNode hd = null, tl = null;
do {
//聲明樹形節點,內容和當前鏈表節點e一致
TreeNode 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);
}
}
TreeNode replacementTreeNode(Node p, Node next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
複製代碼
紅黑樹轉換步驟:
①.桶中第一個結點做爲紅黑樹根節點(黑色)
②.遍歷其餘結點,從根結點開始經過比較哈希值尋找其位置
③.若x結點hash值小於p結點hash值,往p結點左邊尋找,不然往p結點右邊尋找
④.一直按照步驟③尋找,直至尋找的位置爲null即此位置爲x的目標位置
⑤.由於紅黑樹性質,其插入刪除都須要平衡調整
⑥.最後確保紅黑樹根結點爲桶中第一個節點
數據結構
final void treeify(Node[] tab) {
TreeNode root = null;
for (TreeNode x = this, next; x != null; x = next) {
next = (TreeNode)x.next;
x.left = x.right = null;
//第一個結點爲根結點,必須黑色
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class kc = null;
// 若不是根結點從根節點遍歷全部結點與結點x比較哈希值找到其位置
for (TreeNode p = root;;) {
int dir, ph;
K pk = p.key;
// p哈希值大於x哈希值,dir爲-1,從p的左邊找
if ((ph = p.hash) > h)
dir = -1;
// p哈希值小於x哈希值,dir爲1,從p的右邊找
else if (ph < h)
dir = 1;
//哈希值相等時
else if ((kc == null &&
//comparableClassFor方法若x的Key實現了Comparable接口
//返回Key的運行時類型,不然返回null
(kc = comparableClassFor(k)) == null) ||
//compareComparables方法,若pk與x的key類型相同
//返回k.compareTo(pk),不然返回0
(dir = compareComparables(kc, k, pk)) == 0)
//若pk與k哈希值相同沒法比較,直接比較它們的引用地址
//紅黑樹不像avl樹同樣須要高度平衡,其容許局部不多的不徹底平衡
//這樣對於效率影響不大省去了不少沒有必要的調平衡操做
dir = tieBreakOrder(k, pk);
//將p做爲x的父節點,用於給下面的x父節點賦值
TreeNode xp = p;
//若dir不大於0則向p左邊查找,不然向p右邊查找
//若是爲null則表示該位置爲x的目標位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
//dir不大於0,x爲p的左節點
if (dir <= 0)
xp.left = x;
//dir大於0,x爲p的右節點
else
xp.right = x;
//保證插入後平衡
root = balanceInsertion(root, x);
break;
}
}
}
}
//確保根節點是桶的第一個節點
moveRootToFront(tab, root);
}
複製代碼
moveRootToFront方法:多線程
static void moveRootToFront(Node[] tab, TreeNode root) {
int n;
//哈希表不爲空且桶中有節點
if (root != null && tab != null && (n = tab.length) > 0) {
int index = (n - 1) & root.hash;
//記錄桶中第一個元素
TreeNode first = (TreeNode)tab[index];
//若根節點不是桶中第一個節點
if (root != first) {
Node rn;
//將根節點設爲桶中第一個節點
tab[index] = root;
//獲取根節點的前驅節點
TreeNode rp = root.prev;
//若根節點後繼不爲空則將其前驅指向根節點前驅節點
if ((rn = root.next) != null)
((TreeNode)rn).prev = rp;
//若根節點前驅節點不爲null則將其後繼指向根節點的後驅節點
if (rp != null)
rp.next = rn;
//若原桶中第一個結點不爲null則將其前驅指向根節點
if (first != null)
first.prev = root;
//根節點後繼指向原桶中第一個結點
root.next = first;
//根節點前驅設爲null
root.prev = null;
}
//斷言檢測其是否符合紅黑樹性質
assert checkInvariants(root);
}
}
複製代碼
①.分支1:若原哈希表不爲空,判斷其容量是否超過最大容量,若超過則將其閾值設爲int最大值返回原哈希表沒法擴容,若沒超過再判斷原哈希表容量的2倍是否最大容量且不小於16,符合條件將閾值設爲原閾值兩倍
②.分支2:若原哈希表容量爲0,閾值大於0(初始化容量0的HashMap),將新表容量設爲原表的閾值
③.分支3:若原哈希表容量爲0,閾值也爲0(無參構造),將容量和閾值設爲默認值
④.當分支2成立,計算新的resize上限(正常狀況新閾值爲新容量*負載因子)
⑤.將計算好的新閾值設爲當前閾值,以計算好的新容量定義新表
⑥.若原哈希表不爲空則將其結點轉移到新table中
app
/**
* 對哈希表初始化或擴容
* 若哈希表爲null則對其進行初始化
* 擴容後結點要麼原位置,要麼在原位置偏移舊容量的位置
*/
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;
}
//若當前容量的兩倍小於最大容量且當前容量不小於默認初始容量(16)時
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//將新閾值設爲原閾值兩倍
newThr = oldThr << 1; // double threshold
}
//若當前容量爲0且當前閾值大於0
else if (oldThr > 0) // initial capacity was placed in threshold
//將新容量設置成當前擴容閾值
newCap = oldThr;
//若當前容量爲0且當前閾值爲0
else { // zero initial threshold signifies using defaults
// 新容量設爲默認初始化容量
newCap = DEFAULT_INITIAL_CAPACITY;
// 設置新閾值
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//計算新的resize上限
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
//若新容量 < 最大容量且ft < 最大容量,新閾值爲ft,不然爲int最大值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//將閾值設爲newThr
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//建立新數組
Node[] newTab = (Node[])new Node[newCap];
//將當前哈希表設爲擴容後的newTab
table = newTab;
//若原哈希表不爲空則將其結點轉移到新table中
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node e;
//若原桶中有結點
if ((e = oldTab[j]) != null) {
//將舊桶置空便於gc
oldTab[j] = null;
//若桶中只有一個結點,從新計算位置添加
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//若桶結構爲紅黑樹
else if (e instanceof TreeNode)
//分割樹中結點
((TreeNode)e).split(this, newTab, j, oldCap);
//鏈式優化重hash的代碼塊
else { // preserve order
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;
}
// 原索引+oldCap
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;
}
// 原索引+oldCap放到桶中
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
複製代碼
java8的resize方法具備了擴容和初始化功能相對於7,其沒有從新計算hash值,只須要看新增的1bit是0仍是1能夠認爲是隨機的,所以resize過程,均勻地把以前的衝突結點分散到新的bucket,這塊即是java8新增的優化點,而且java7rehash時新表的數組索引位置相同,鏈表元素會倒置也由於倒置多線程可能會出現死循環,而java8順序一致不會出現此場景.
post
final void split(HashMap map, Node[] tab, int index, int bit) {
//獲取調用此方法結點
TreeNode b = this;
//存儲與原索引位置相同的結點
TreeNode loHead = null, loTail = null;
//存儲原索引+oldCap的結點
TreeNode hiHead = null, hiTail = null;
int lc = 0, hc = 0;
for (TreeNode e = b, next; e != null; e = next) {
next = (TreeNode)e.next;
e.next = null;
//擴容後與原位置相同,尾插
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
//擴容後位置爲原位置+舊容量,尾插
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
//擴容原位置桶中仍有結點
if (loHead != null) {
//桶中結點數少於6個將紅黑樹轉爲鏈表
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
//從新構建紅黑樹
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
//索引爲原位置+原容量位置桶上的有結點
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
複製代碼
/**
* 紅黑樹轉爲鏈表
*/
final Node untreeify(HashMap map) {
Node hd = null, tl = null;
//從調用此方法結點開始遍歷,將全部結點轉爲鏈表結點
for (Node q = this; q != null; q = q.next) {
Node p = map.replacementNode(q, null);
//第一個結點爲頭結點,其他逐個尾插
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
複製代碼
最主要講了put方法、擴容機制以及鏈表與樹間轉換,java8中HashMap引入了紅黑樹,由於紅黑樹查詢時間複雜度爲O(logn)能解決較多哈希衝突問題,因此簡化了其hash算法,採用尾插方式添加結點.其次擴容也不須要從新計算hash值,擴容後的結點位置(原位置/偏移舊容量)均勻地把以前的衝突結點分散到新的桶中,且不會出現死循環,可是其依舊線程不安全.
在本節中並無說起get方法,由於當理解了put原理,get操做與之雷同.也未涉及太多紅黑樹相關東西,我想等介紹treemap再詳細討論.
1.爲何轉紅黑樹的閾值是8?
咱們能夠從HashMap源碼中有一段註釋說明,理想狀況下使用隨機的哈希碼,容器中結點分佈在hash桶中的頻率遵循泊松分佈(詳情),按照泊松分佈的計算公式計算出了桶中元素個數和機率的對照表能夠看到鏈表中元素個數爲8時的機率已經很是小,再多的就更少了,因此選擇了8.
桶中元素個數和機率的關係以下:
數量 | 機率 |
0 | 0.60653066 |
1 | 0.30326533 |
2 | 0.07581633 |
3 | 0.01263606 |
4 | 0.00157952 |
5 | 0.00015795 |
6 | 0.00001316 |
7 | 0.00000094 |
8 | 0.00000006 |
https://blog.csdn.net/v123411739/article/details/78996181 https://tech.meituan.com/java-hashmap.html https://www.toutiao.com/a6542437571140518414/