国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學院 > 開發設計 > 正文

圖解集合5:不正確地使用HashMap引發死循環及元素丟失

2019-11-14 14:49:33
字體:
來源:轉載
供稿:網友

問題引出

前一篇文章講解了HashMap的實現原理,講到了HashMap不是線程安全的。那么HashMap在多線程環境下又會有什么問題呢?

幾個月前,公司項目的一個模塊在線上運行的時候出現了死循環,死循環的代碼就卡在HashMap的get方法上。盡管最終發現不是因為HashMap導致的,但卻讓我重視了HashMap在多線程環境下會引發死循環的這個問題,下面先用一段代碼簡單模擬出HashMap的死循環:

public class HashMapThread extends Thread{    PRivate static AtomicInteger ai = new AtomicInteger(0);    private static Map<Integer, Integer> map = new HashMap<Integer, Integer>(1);        public void run()    {        while (ai.get() < 100000)        {            map.put(ai.get(), ai.get());            ai.incrementAndGet();        }    }}

這個線程的作用很簡單,給AtomicInteger不斷自增并寫入HashMap中,其中AtomicInteger和HashMap都是全局共享的,也就是說所有線程操作的都是同一個AtomicInteger和HashMap。開5個線程操作一下run方法中的代碼:

public static void main(String[] args){    HashMapThread hmt0 = new HashMapThread();    HashMapThread hmt1 = new HashMapThread();    HashMapThread hmt2 = new HashMapThread();    HashMapThread hmt3 = new HashMapThread();    HashMapThread hmt4 = new HashMapThread();    hmt0.start();    hmt1.start();    hmt2.start();    hmt3.start();    hmt4.start();}

多運行幾次之后死循環就出來了,我大概運行了7次、8次的樣子,其中有幾次是數組下標越界異常ArrayIndexOutOfBoundsException。這里面要提一點,多線程環境下代碼會出現問題并不意味著多線程環境下一定會出現問題,但是只要出現了問題,或者是死鎖、或者是死循環,那么你的項目除了重啟就沒有什么別的辦法了,所以代碼的線程安全性在開發、評審的時候必須要重點考慮到。OK,看一下控制臺:

紅色方框一直亮著,說明代碼死循環了。死循環問題的定位一般都是通過jps+jstack查看堆棧信息來定位的:

看到Thread-0處于RUNNABLE,而從堆棧信息上應該可以看出,這次的死循環是由于Thread-0對HashMap進行擴容而引起的。

所以,本文就解讀一下,HashMap的擴容為什么會引起死循環。

 

正常的擴容過程

先來看一下HashMap一次正常的擴容過程。簡單一點看吧,假設我有三個經過了最終rehash得到的數字,分別是5 7 3,HashMap的table也只有2,那么HashMap把這三個數字put進數據結構了之后應該是這么一個樣子的:

這應該很好理解。然后看一下resize的代碼,上面的堆棧里面就有:

void addEntry(int hash, K key, V value, int bucketIndex) {Entry<K,V> e = table[bucketIndex];    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);    if (size++ >= threshold)        resize(2 * table.length);}
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);    table = newTable;    threshold = (int)(newCapacity * loadFactor);}
void transfer(Entry[] newTable) {    Entry[] src = table;    int newCapacity = newTable.length;    for (int j = 0; j < src.length; j++) {        Entry<K,V> e = src[j];        if (e != null) {            src[j] = null;            do {                Entry<K,V> next = e.next;                int i = indexFor(e.hash, newCapacity);                e.next = newTable[i];                newTable[i] = e;                e = next;            } while (e != null);        }    }}

我總結一下這三段代碼,HashMap一次擴容的過程應該是:

1、取當前table的2倍作為新table的大小

2、根據算出的新table的大小new出一個新的Entry數組來,名為newTable

3、輪詢原table的每一個位置,將每個位置上連接的Entry,算出在新table上的位置,并以鏈表形式連接

4、原table上的所有Entry全部輪詢完畢之后,意味著原table上面的所有Entry已經移到了新的table上,HashMap中的table指向newTable

這樣就完成了一次擴容,用圖表示是這樣的:

HashMap的一次正常擴容就是這樣的,這很好理解。

 

擴容導致的死循環

既然是擴容導致的死循環,那么繼續看擴容的代碼:

 1 void transfer(Entry[] newTable) { 2     Entry[] src = table; 3     int newCapacity = newTable.length; 4     for (int j = 0; j < src.length; j++) { 5         Entry<K,V> e = src[j]; 6         if (e != null) { 7             src[j] = null; 8             do { 9                 Entry<K,V> next = e.next;10                 int i = indexFor(e.hash, newCapacity);11                 e.next = newTable[i];12                 newTable[i] = e;13                 e = next;14             } while (e != null);15         }16     }17 }

兩個線程,線程A和線程B。假設第9行執行完畢,線程A切換,那么對于線程A而言,是這樣的:

CPU切換到線程B運行,線程B將整個擴容過程全部執行完畢,于是就形成了:

此時CPU切換到線程A上,執行第8行~第14行的do...while...循環,首先放置3這個Entry:

我們必須要知道,由于線程B已經執行完畢,因此根據java內存模型(JMM),現在table里面所有的Entry都是最新的,也就是7的next是3,3的next是null。3放置到table[3]的位置上了,下面的步驟是:

1、e=next,即e=7

2、判斷e不等于null,循環繼續

3、next=e.next,即next=7的next,也就是3

4、放置7這個Entry

所以,用圖表示就是:

放置完7之后,繼續運行代碼:

1、e=next,也就是說e=3

2、判斷e不等于null,循環繼續

3、next=e.next,即3的next,也就是null

4、放置3這個Entry

把3移到table[3]上去,死循環就出來了:

3移到table[3]上去了,3的next指向7,由于原先7的next指向3,這樣就成了一個死循環。

此時執行13行的e=next,那么e=null,循環終止。盡管此次循環確實結束了,但是后面的操作,只要涉及輪詢HashMap數據結構的,無論是迭代還是擴容,都將在table[3]這個鏈表處出現死循環。這也就是前面的死循環堆棧出現的原因,transfer的484行,因為這是一次擴容操作,需要遍歷HashMap數據結構,transfer方法是擴容的最后一個方法。

 

3 5 7又會有怎樣的結果

可能有人覺得上面的數字5 7 3太巧了,像是專門為了產生HashMap的死循環而故意選擇的數字。

這個問題,我這么回答:我記得在《從Paxos到Zookeeper分布式一致性原理與實踐》有一段話大概是這么描述的,有一個被反復實踐得出的結論是,任何在多線程下可能發生的錯誤場景最終一定會發生

5 7 3這個數字可不巧,擴容前相鄰兩個Entry被分配到擴容后同樣的table位置是很正常的。關鍵的是,即使這種異常場景發生的可能性再低,只要發生一次,那么你的系統就部分甚至全部不可用了----除了重啟系統沒有任何辦法。所以,這種可能會發生的異常場景必須提前扼殺。

OK,不扯了,前面講了5 7 3導致了死循環,現在看一下正常的順序3 5 7,會發生什么問題。簡單看一下,就不像上面講得這么詳細了:

這是擴容前數據結構中的內容,擴容之后正常的應該是:

現在在多線程下遇到問題了,某個線程先放7:

 

再接著放5:

由于5的next此時為null,因此擴容操作結束,3 5 7造成的結果就是元素丟失。

 

如何解決

把一個線程非安全的集合作為全局共享的,本身就是一種錯誤的做法,并發下一定會產生錯誤。

所以,解決這個問題的辦法很簡單,有兩種:

1、使用Collections.synchronizedMap(Mao<K,V> m)方法把HashMap變成一個線程安全的Map

2、使用Hashtable、ConcurrentHashMap這兩個線程安全的Map

不過,既然選擇了線程安全的辦法,那么必然要在性能上付出一定的代價----畢竟這個世界上沒有十全十美的事情,既要運行效率高、又要線程安全。

 


發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 德钦县| 景德镇市| 金堂县| 涡阳县| 咸阳市| 永泰县| 谷城县| 甘南县| 辽源市| 贵德县| 连州市| 隆昌县| 晋中市| 苏尼特右旗| 察雅县| 阜宁县| 卢湾区| 桃园县| 托克托县| 赫章县| 泰宁县| 慈溪市| 军事| 德江县| 金塔县| 双江| 雅江县| 达州市| 息烽县| 汤原县| 东台市| 美姑县| 离岛区| 襄汾县| 德惠市| 安远县| 鲁山县| 化隆| 林芝县| 秦皇岛市| 济宁市|