本文作者: Code皮皮蝦,CSDN互躬、掘金等各大平臺同名播赁,有興趣的小伙伴可以點一波關注??,感謝您的支持吨铸!
?持續(xù)分享大廠面試題?
公眾號:JavaCodes
前言
HashMap在JDK7和JDK8是有了一些不同的行拢,具體體現(xiàn)如下:
- JDK7HashMap底層是數(shù)組+鏈表,而JDK8是數(shù)組+鏈表+紅黑樹
- JDK7擴容采用頭插法,而JDK8采用尾插法
- JDK7的rehash是全部rehash舟奠,而JDK8是部分rehash竭缝。
- JDK8對于key的hash值計算相比于JDK7來說有所優(yōu)化。
如果還有興趣的小伙伴可以學習學習我的以下文章沼瘫,寫的十分詳細Lе健!
JDK7湿故、8HashMap的get()、put()流程詳解
JDK7 HashMap
JDK7HashMap在多線程環(huán)境下會出現(xiàn)死循環(huán)問題膜蛔。
假如此時A坛猪、B線程同時對一個HashMap進行put操作,且HashMap剛號達到擴容條件需要進行擴容
那么這兩個線程都會取對HahsMap進行擴容(JDK7HashMap擴容調用 resize()方法皂股,而resize()方法中需要調用transfer()方法將舊數(shù)組元素全部rehash到新數(shù)組中去==重點:這里在多線程環(huán)境下就會出現(xiàn)問題==)
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);
}
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//對數(shù)組的每一條鏈表遍歷rehash
for (Entry<K,V> e : table) {
while(null != e) {
//保留下一個節(jié)點
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//得到對應在新數(shù)組中的索引位置
int i = indexFor(e.hash, newCapacity);
//尾插法
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
我們假設現(xiàn)在有一個鏈表 C——>D,且C墅茉、D擴容后計算的索引位置依然不變,那他么還在同一鏈表中
現(xiàn)在A線程進入到transfer方法拿到C和它的下一個節(jié)點D(Entry<K,V> next = e.next;)后呜呐,A線程被掛起就斤,此時B線程正常走流程將C、D rehash到新的數(shù)組中蘑辑,那么根據(jù)頭插法在新的數(shù)組中是D——>C
B執(zhí)行完之后洋机,A線程繼續(xù)去執(zhí)行
因為A獲取到了 e = C,next = D,所以C可以進行rehash,C進行完后拿到D洋魂,發(fā)現(xiàn)D.next = C,所以D也可以進行rehash绷旗,那么此時因為D——>C,此時會再拿到C,發(fā)現(xiàn)C.next = null副砍,但C不是null刁标,所以C再進行rehash,此時鏈表尾 C——> D ——>C,因為此時e = NULL址晕,所以退出循環(huán),此時出現(xiàn)死循環(huán)顿锰。C——>D——>C谨垃。
==各位可以好好想想這些話或者自己在草稿紙上畫一畫再來看下面的圖!==
圖示演示:
==B正常執(zhí)行完成==
==A繼續(xù)執(zhí)行==
因為A獲取到了 e = C,next = D,所以C可以進行rehash
C進行完后拿到e = D硼控,發(fā)現(xiàn)D.next = C,所以D也可以進行rehash
那么此時因為D——>C,此時會再拿到C刘陶,發(fā)現(xiàn)C.next = null,但C不是null牢撼,所以C再進行rehash
此時e = NULL匙隔,所以退出循環(huán),此時出現(xiàn)死循環(huán)熏版。C——>D——>C纷责。
JDK8 HashMap
JDK1.8會出現(xiàn)數(shù)據(jù)覆蓋的情況
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> 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<K,V> 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<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;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
- ==第6行代碼==:假設兩個線程A捍掺、B都在進行put操作,并且根據(jù)key計算出的hash值相同再膳,那么得到得索引下標也相同挺勿,當線程A執(zhí)行完第六行代碼后由于時間片耗盡導致被掛起,而線程B得到時間片后在該下標處插入了元素喂柒,完成了正常的插入不瓶,然后線程A獲得時間片,由于之前已經進行了hash碰撞的判斷灾杰,所有此時不會再進行判斷蚊丐,而是直接進行插入,這就導致了線程B插入的數(shù)據(jù)被線程A覆蓋了艳吠,從而線程不安全麦备。
- ==第38行代碼==++size不安全,還是線程A讲竿、B泥兰,這兩個線程同時進行put操作時,假設當前HashMap的zise大小為10题禀,當線程A執(zhí)行到第38行代碼時鞋诗,從主內存中獲得size的值為10后準備進行+1操作,但是由于時間片耗盡只好讓出CPU迈嘹,線程B快樂的拿到CPU還是從主內存中拿到size的值10進行+1操作削彬,完成了put操作并將size=11寫回主內存,然后線程A再次拿到CPU并繼續(xù)執(zhí)行(此時size的值仍為10)秀仲,當執(zhí)行完put操作后融痛,還是將size=11寫回內存,此時神僵,線程A雁刷、B都執(zhí)行了一次put操作,但是size的值只增加了1保礼,所有說還是由于數(shù)據(jù)覆蓋又導致了線程不安全沛励。
最后
我是 Code皮皮蝦,一個熱愛分享知識的 皮皮蝦愛好者炮障,未來的日子里會不斷更新出對大家有益的博文目派,期待大家的關注!P灿企蹭!
創(chuàng)作不易,如果這篇博文對各位有幫助,希望各位小伙伴可以一鍵三連哦谅摄!徒河,感謝支持,我們下次再見~~~
==分享大綱==