谈谈HashMap线程不安全的体现
轉(zhuǎn)載自?談?wù)凥ashMap線程不安全的體現(xiàn)
HashMap的原理以及如何實(shí)現(xiàn),之前在JDK7與JDK8中HashMap的實(shí)現(xiàn)中已經(jīng)說明了。
那么,為什么說HashMap是線程不安全的呢?它在多線程環(huán)境下,會(huì)發(fā)生什么情況呢?
1. resize死循環(huán)
我們都知道HashMap初始容量大小為16,一般來說,當(dāng)有數(shù)據(jù)要插入時(shí),都會(huì)檢查容量有沒有超過設(shè)定的thredhold,如果超過,需要增大Hash表的尺寸,但是這樣一來,整個(gè)Hash表里的元素都需要被重算一遍。這叫rehash,這個(gè)成本相當(dāng)?shù)拇蟆?/p>
| 12345678910111213 | 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);} |
| 123456789101112131415 | void transfer(Entry[] newTable, boolean rehash) {????????int newCapacity = newTable.length;????????for (Entry<K,V> e : table) {????????????while(null != e) {????????????????Entry<K,V> next = e.next;????????????????if (rehash) {????????????????????e.hash = null == e.key ? 0 : hash(e.key);????????????????}????????????????int i = indexFor(e.hash, newCapacity);????????????????e.next = newTable[i];????????????????newTable[i] = e;????????????????e = next;????????????}????????}} |
大概看下transfer:
經(jīng)過這幾步,我們會(huì)發(fā)現(xiàn)轉(zhuǎn)移的時(shí)候是逆序的。假如轉(zhuǎn)移前鏈表順序是1->2->3,那么轉(zhuǎn)移后就會(huì)變成3->2->1。這時(shí)候就有點(diǎn)頭緒了,死鎖問題不就是因?yàn)?->2的同時(shí)2->1造成的嗎?所以,HashMap 的死鎖問題就出在這個(gè)transfer()函數(shù)上。
1.1?單線程 rehash 詳細(xì)演示
單線程情況下,rehash 不會(huì)出現(xiàn)任何問題:
- 假設(shè)hash算法就是最簡(jiǎn)單的 key mod table.length(也就是數(shù)組的長(zhǎng)度)。
- 最上面的是old hash 表,其中的Hash表的 size = 2, 所以 key = 3, 7, 5,在 mod 2以后碰撞發(fā)生在 table[1]
- 接下來的三個(gè)步驟是 Hash表 resize 到4,并將所有的?<key,value>?重新rehash到新 Hash 表的過程
如圖所示:
?
1.2?多線程 rehash 詳細(xì)演示
為了思路更清晰,我們只將關(guān)鍵代碼展示出來
| 123456 | while(null != e) {????Entry<K,V> next = e.next;????e.next = newTable[i];????newTable[i] = e;????e = next;} |
假設(shè)這里有兩個(gè)線程同時(shí)執(zhí)行了put()操作,并進(jìn)入了transfer()環(huán)節(jié)
| 123456 | while(null != e) {????Entry<K,V> next = e.next; //線程1執(zhí)行到這里被調(diào)度掛起了????e.next = newTable[i];????newTable[i] = e;????e = next;} |
那么現(xiàn)在的狀態(tài)為:
?
從上面的圖我們可以看到,因?yàn)榫€程1的 e 指向了 key(3),而 next 指向了 key(7),在線程2 rehash 后,就指向了線程2 rehash 后的鏈表。
然后線程1被喚醒了:
然后該執(zhí)行 key(3)的 next 節(jié)點(diǎn) key(7)了:
這時(shí)候的狀態(tài)圖為:
?
然后又該執(zhí)行 key(7)的 next 節(jié)點(diǎn) key(3)了:
這時(shí)候的狀態(tài)如圖所示:
?
很明顯,環(huán)形鏈表出現(xiàn)了!!當(dāng)然,現(xiàn)在還沒有事情,因?yàn)橄乱粋€(gè)節(jié)點(diǎn)是 null,所以transfer()就完成了,等put()的其余過程搞定后,HashMap 的底層實(shí)現(xiàn)就是線程1的新 Hash 表了。
2. fail-fast
如果在使用迭代器的過程中有其他線程修改了map,那么將拋出ConcurrentModificationException,這就是所謂fail-fast策略。
這個(gè)異常意在提醒開發(fā)者及早意識(shí)到線程安全問題,具體原因請(qǐng)查看ConcurrentModificationException的原因以及解決措施
順便再記錄一個(gè)HashMap的問題:
為什么String, Interger這樣的wrapper類適合作為鍵??String, Interger這樣的wrapper類作為HashMap的鍵是再適合不過了,而且String最為常用。因?yàn)镾tring是不可變的,也是final的,而且已經(jīng)重寫了equals()和hashCode()方法了。其他的wrapper類也有這個(gè)特點(diǎn)。不可變性是必要的,因?yàn)闉榱艘?jì)算hashCode(),就要防止鍵值改變,如果鍵值在放入時(shí)和獲取時(shí)返回不同的hashcode的話,那么就不能從HashMap中找到你想要的對(duì)象。不可變性還有其他的優(yōu)點(diǎn)如線程安全。如果你可以僅僅通過將某個(gè)field聲明成final就能保證hashCode是不變的,那么請(qǐng)這么做吧。因?yàn)楂@取對(duì)象的時(shí)候要用到equals()和hashCode()方法,那么鍵對(duì)象正確的重寫這兩個(gè)方法是非常重要的。如果兩個(gè)不相等的對(duì)象返回不同的hashcode的話,那么碰撞的幾率就會(huì)小些,這樣就能提高HashMap的性能。
Reference:
1.?http://hwl-sz.iteye.com/blog/1897468?utm_source=tuicool&utm_medium=referral
2.?http://github.thinkingbar.com/hashmap-infinite-loop/
3.?http://www.importnew.com/7099.html
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)
總結(jié)
以上是生活随笔為你收集整理的谈谈HashMap线程不安全的体现的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: iphone重力感应关闭方法是什么
- 下一篇: HashMap的实现与优化