首先需要強調壹點,HashMap的線程不安全體現在會造成死循環、數據丟失、數據覆蓋這些問題。其中死循環和數據丟失是在JDK1.7中出現的問題,在JDK1.8中已經得到解決,然而1.8中仍會有數據覆蓋這樣的問題。
HashMap的線程不安全主要是發生在擴容函數中,即根源是在transfer函數中,JDK1.7中HashMap的transfer函數如下:
這段代碼是HashMap的擴容操作,重新定位每個桶的下標,並采用頭插法將元素遷移到新數組中。頭插法會將鏈表的順序翻轉,這也是形成死循環的關鍵點。理解了頭插法後再繼續往下看是如何造成死循環以及數據丟失的。
擴容造成死循環和數據丟失的分析過程
假設現在有兩個線程A、B同時對下面這個HashMap進行擴容操作:
正常擴容後的結果是下面這樣的:
但是當線程A執行到上面transfer函數的第11行代碼時,CPU時間片耗盡,線程A被掛起。即如下圖中位置所示:
此時線程A中:e=3、next=7、e.next=null
當線程A的時間片耗盡後,CPU開始執行線程B,並在線程B中成功的完成了數據遷移
重點來了,根據Java內存模式可知,線程B執行完數據遷移後,此時主內存中newTable和table都是最新的,也就是說:7.next=3、3.next=null。
隨後線程A獲得CPU時間片繼續執行newTable[i] = e,將3放入新數組對應的位置,執行完此輪循環後線程A的情況如下:
接著繼續執行下壹輪循環,此時e=7,從主內存中讀取e.next時發現主內存中7.next=3,於是乎next=3,並將7采用頭插法的方式放入新數組中,並繼續執行完此輪循環,結果如下:
執行下壹次循環可以發現,next=e.next=null,所以此輪循環將會是最後壹輪循環。接下來當執行完e.next=newTable[i]即3.next=7後,3和7之間就相互連接了,當執行完newTable[i]=e後,3被頭插法重新插入到鏈表中,執行結果如下圖所示:
上面說了此時e.next=null即next=null,當執行完e=null後,將不會進行下壹輪循環。到此線程A、B的擴容操作完成,很明顯當線程A執行完後,HashMap中出現了環形結構,當在以後對該HashMap進行操作時會出現死循環。
並且從上圖可以發現,元素5在擴容期間被莫名的丟失了,這就發生了數據丟失的問題。
JDK1.8中的線程不安全
根據上面JDK1.7出現的問題,在JDK1.8中已經得到了很好的解決,如果妳去閱讀1.8的源碼會發現找不到transfer函數,因為JDK1.8直接在resize函數中完成了數據遷移。另外說壹句,JDK1.8在進行元素插入時使用的是尾插法。
為什麽說JDK1.8會出現數據覆蓋的情況喃,我們來看壹下下面這段JDK1.8中的put操作代碼:
其中第六行代碼是判斷是否出現hash碰撞,假設兩個線程A、B都在進行put操作,並且hash函數計算出的插入下標是相同的,當線程A執行完第六行代碼後由於時間片耗盡導致被掛起,而線程B得到時間片後在該下標處插入了元素,完成了正常的插入,然後線程A獲得時間片,由於之前已經進行了hash碰撞的判斷,所有此時不會再進行判斷,而是直接進行插入,這就導致了線程B插入的數據被線程A覆蓋了,從而線程不安全。
除此之前,還有就是代碼的第38行處有個++size,我們這樣想,還是線程A、B,這兩個線程同時進行put操作時,假設當前HashMap的zise大小為10,當線程A執行到第38行代碼時,從主內存中獲得size的值為10後準備進行+1操作,但是由於時間片耗盡只好讓出CPU,線程B快樂的拿到CPU還是從主內存中拿到size的值10進行+1操作,完成了put操作並將size=11寫回主內存,然後線程A再次拿到CPU並繼續執行(此時size的值仍為10),當執行完put操作後,還是將size=11寫回內存,此時,線程A、B都執行了壹次put操作,但是size的值只增加了1,所有說還是由於數據覆蓋又導致了線程不安全。
總結
HashMap的線程不安全主要體現在下面兩個方面:
1.在JDK1.7中,當並發執行擴容操作時會造成環形鏈和數據丟失的情況。
2.在JDK1.8中,在並發執行put操作時會發生數據覆蓋的情況。
轉自:
網頁鏈接