HashMap原始碼解讀 JDK7/8
HashMap 原理
資料結構
JDK 8 和JDK 7 的區別
1.為了加快查詢效率,java8的hashmap引入了紅黑樹結構,當陣列長度大於預設閾值64時,且當某一連結串列的元素>8時,該連結串列就會轉成紅黑樹結構,查詢效率更高。(問題來了,什麼是紅黑樹?什麼是B+樹?(mysql索引有B+樹索引)什麼是B樹?什麼是二叉查詢樹?)資料結構方面的知識點會更新在【資料結構專題】,這裡不展開。
這裡只簡單的介紹一下紅黑樹:
紅黑樹是一種自平衡二叉樹,擁有優秀的查詢和插入/刪除效能,廣泛應用於關聯陣列。對比AVL樹,AVL要求每個結點的左右子樹的高度之差的絕對值(平衡因子)最多為1,而紅黑樹通過適當的放低該條件(紅黑樹限制從根到葉子的最長的可能路徑不多於最短的可能路徑的兩倍長,結果是這個樹大致上是平衡的),以此來減少插入/刪除時的平衡調整耗時,從而獲取更好的效能,而這雖然會導致紅黑樹的查詢會比AVL稍慢,但相比插入/刪除時獲取的時間,這個付出在大多數情況下顯然是值得的。
2.優化擴容方法,在擴容時保持了原來連結串列中的順序,避免出現死迴圈
HashMap 的儲存結構是一個 Entry陣列,和一個單向連結串列。JDK1.8之後 連結串列結構如果超過 8 連結串列變成為紅黑樹
容量(capacity):每次擴容是 2*n 一定是2的n次方?!原始碼中有個roundUpToPowerOf2(toSize); 方法 20 ->32
負責因子(loadFactor): 0.75
擴容閥值 (threshold):0.75*capacity 到到這個值 hashmap擴容 <<=1
與hashtable比較
hashtabe 初始值 11 每次擴容 2*n+1
hashtable 採用的是synchronized 加鎖,保證執行緒安全,推薦使用concurrentHashMap 採用分段鎖的方式,效率更高
JDK 7 中hashmap 怎麼 put,get?
public V put(K key, V value) {
// 當插入第一個元素的時候,需要先初始化陣列大小
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 如果 key 為 null,感興趣的可以往裡看,最終會將這個 entry 放到 table[0] 中
if (key == null)
return putForNullKey(value);
// 1. 求 key 的 hash 值
int hash = hash(key);
// 2. 找到對應的陣列下標
int i = indexFor(hash, table.length);
// 3. 遍歷一下對應下標處的連結串列,看是否有重複的 key 已經存在,
// 如果有,直接覆蓋,put 方法返回舊值就結束了
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 4. 不存在重複的 key,將此 entry 新增到連結串列中,細節後面說
addEntry(hash, key, value, i);
return null;
}
先看陣列初始化的函式 inflateTable
private void inflateTable(int toSize) {
// 保證陣列大小一定是 2 的 n 次方。
// 比如這樣初始化:new HashMap(20),那麼處理成初始陣列大小是 32
int capacity = roundUpToPowerOf2(toSize);
// 計算擴容閾值:capacity * loadFactor
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// 算是初始化陣列吧
table = new Entry[capacity];
initHashSeedAsNeeded(capacity); //ignore
}
找到陣列下標
原理: 使用 key 的 hash 值對陣列長度進行取模就可以了, 這個也是因為長度是 2的n次方才可以這麼幹
static int indexFor(int hash, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return hash & (length-1);
}
新增節點到連結串列中
原理:找到陣列下標後,會先進行 key 判重,如果沒有重複,就準備將新值放入到連結串列的表頭。
void addEntry(int hash, K key, V value, int bucketIndex) {
// 如果當前 HashMap 大小已經達到了閾值,並且新值要插入的陣列位置已經有元素了,那麼要擴容
if ((size >= threshold) && (null != table[bucketIndex])) {
// 擴容,後面會介紹一下
resize(2 * table.length);
// 擴容以後,重新計算 hash 值
hash = (null != key) ? hash(key) : 0;
// 重新計算擴容後的新的下標
bucketIndex = indexFor(hash, table.length);
}
// 往下看
createEntry(hash, key, value, bucketIndex);
}
// 這個很簡單,其實就是將新值放到連結串列的表頭,然後 size++
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
新增節點最重要的是先判斷是否需要擴容,需要的話先擴容,再把節點放到擴容後的陣列相應位置的連結串列的表頭。
陣列擴容
插入新值時,如果當前的陣列已經到達了陣列的閥值,並且插入的陣列上已經有元素,會觸發擴容,擴容後,陣列為原來的兩倍
擴容時將一個新的陣列替換為原來的小陣列,並且將原陣列中的值插入到新陣列中去。
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 新的陣列
Entry[] newTable = new Entry[newCapacity];
// 將原來陣列中的值遷移到新的更大的陣列中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
由於是雙倍擴容,遷移過程中,會將原來 table[i] 中的連結串列的所有節點,分拆到新的陣列的 newTable[i] 和 newTable[i + oldLength] 位置上。如原來陣列長度是 16,那麼擴容後,原來 table[0] 處的連結串列中的所有元素會被分配到新陣列中 newTable[0] 和 newTable[16] 這兩個位置。
陣列遷移 transfer
void transfer(Entry[] newTable) {
Entry[] src = table; //src引用了舊的Entry陣列
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) { //遍歷舊的Entry陣列
Entry<K,V> e = src[j]; //取得舊Entry陣列的每個元素
if (e != null) {
src[j] = null;//釋放舊Entry陣列的物件引用(for迴圈後,舊的Entry陣列不再引用任何物件)
do {// 頭插法建立連結串列
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity); //!!重新計算每個元素在陣列中的位置
e.next = newTable[i]; //標記[1]
newTable[i] = e; //將元素放在陣列上
e = next; //訪問下一個Entry鏈上的元素
} while (e != null);
}
}
}
總結: JDK7中HashMap如何put
putval方法:
判斷table 是否為空或者null ,如果是,進行resize擴容
如果 key = null 這個key 防止entry 陣列的 table[0]
如果key !=null 計算key的hash 值, 然後根據hash值 找到資料的下標
遍歷這個陣列下的連結串列:
如果連結串列下有相同的key值,用這個新節點Node(K,V)的V 替代原來舊的value
如果連結串列下沒有相同的key,在連結串列中增加, 如果size>threadhold 時候,進行擴容,此時需要根據hash重新計算陣列下標,最後將節點加入到連結串列的表頭,方便查詢
hashmap 如何 get?
- 根據 key 計算 hash 值。
- 找到相應的陣列下標:hash & (length – 1)。
- 遍歷該陣列位置處的連結串列,直到找到相等(==或equals)的 key。
根據key值得到value
public V get(Object key) {
// 之前說過,key 為 null 的話,會被放到 table[0],所以只要遍歷下 table[0] 處的連結串列就可以了
if (key == null)
return getForNullKey();
//
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
getEntry(key)
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
// 確定陣列下標,然後從頭開始遍歷連結串列,直到找到為止
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
總結:JDK7中HashMap如何get?
如果key=null 返回的是tabe[0] 的連結串列,根據連結串列去找值就可以了
如果key!=null key做hash,找到陣列下標,然後遍歷連結串列,根據key值比較,找到對應Node值,然後返回.
JDK1.8 中HashMap的資料結構
JDK8中對HashMap的結構做了一定的修改。JDK8中引入了紅黑樹,資料結構為 陣列+紅黑樹+連結串列
JDK8中HashMap如何put?
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 第三個引數 onlyIfAbsent 如果是 true,那麼只有在不存在該 key 時才會進行 put 操作
// 第四個引數 evict 我們這裡不關心
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 第一次 put 值的時候,會觸發下面的 resize(),類似 java7 的第一次 put 也要初始化陣列長度
// 第一次 resize 和後續的擴容有些不一樣,因為這次是陣列從 null 初始化到預設的 16 或自定義的初始容量
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 找到具體的陣列下標,如果此位置沒有值,那麼直接初始化一下 Node 並放置在這個位置就可以了
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {// 陣列該位置有資料
Node<K,V> e; K k;
// 首先,判斷該位置的第一個資料和我們要插入的資料,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) {
// 插入到連結串列的最後面(Java7 是插入到連結串列的最前面)
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// TREEIFY_THRESHOLD 為 8,所以,如果新插入的值是連結串列中的第 9 個
// 會觸發下面的 treeifyBin,也就是將連結串列轉換為紅黑樹
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果在該連結串列中找到了"相等"的 key(== 或 equals)
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 此時 break,那麼 e 為連結串列中[與要插入的新值的 key "相等"]的 node
break;
p = e;
}
}
// e!=null 說明存在舊值的key與要插入的key"相等"
// 對於我們分析的put操作,下面這個 if 其實就是進行 "值覆蓋",然後返回舊值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 如果 HashMap 由於新插入這個值導致 size 已經超過了閾值,需要進行擴容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
JDK7和JDK8中不一樣的地方有JDK7是先擴容然後再放新值,JDK8是先放值後擴容,當然這個不是重點
總結下JDK8中put的思路
- hashmap第一次put的時候,會進行擴容,用來初始化陣列長度。
- 找到陣列下標,判斷該陣列下標對應的元素是否為null,如果是null,直接將新值放進去即可
- 如果陣列下標對應的元素不是null, 如果這個連結串列的第一個資料和要插入的值相等,說明了要覆蓋,後續處理;判斷節點型別是不是紅黑樹,然後使用紅黑樹的插值方法;如果是連結串列,有元素和要插入的值相等的,說明需要覆蓋,後續處理。不同的,插到連結串列最後面,然後判斷是否要轉成紅黑樹。
- 插入值之後,判斷是否需要擴容,需要擴容,走擴容流程。
HashMap擴容 resize()
final Node<K,V>[] resize() {
Node<K,V>[] 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 << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 將閾值擴大一倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // 對應使用 new HashMap(int initialCapacity) 初始化後,第一次 put 的時候
newCap = oldThr;
else {// 對應使用 new HashMap() 初始化後,第一次 put 的時候
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;
// 用新的陣列大小初始化新的陣列
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab; // 如果是初始化陣列,到這裡就結束了,返回 newTab 即可
if (oldTab != null) {
// 開始遍歷原陣列,進行資料遷移。
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 如果該陣列位置上只有單個元素,那就簡單了,簡單遷移這個元素就可以了
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 如果是紅黑樹,具體我們就不展開了
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// 這塊是處理連結串列的情況,
// 需要將此連結串列拆成兩個連結串列,放到新的陣列中,並且保留原來的先後順序
// loHead、loTail 對應一條連結串列,hiHead、hiTail 對應另一條連結串列,程式碼還是比較簡單的
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> 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;
// 第二條連結串列的新的位置是 j + oldCap,這個很好理解
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
JDK8 HashMap擴容和JDK7的有一定區別,JDK7中採用迴圈遍歷連結串列節點的方式擴容,在併發的情況下可能會成環,JDK8中,採用了高 低連結串列的方式,有效的避免了這個問題。
JDK8中HashMap如何get
- 計算 key 的 hash 值,根據 hash 值找到對應陣列下標: hash & (length-1)
- 判斷陣列該位置處的元素是否剛好就是我們要找的,如果不是,走第3步
- 判斷該元素型別是否是 TreeNode,如果是,用紅黑樹的方法取資料,如果不是,走第4步
- 遍歷連結串列,直到找到相等(==或equals)的 key.
public V get(Object key) {
Node<K,V> e;
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;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 判斷第一個節點是不是就是需要的
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 判斷是否是紅黑樹
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 連結串列遍歷
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
http://www.javastack.cn/article/2018/hashmap-concurrenthashmap-details/
https://mp.weixin.qq.com/s/Q5F604CUM_0x2co9mvADxw