點擊 Java編程技術樂園,輕鬆關注!及時獲取有趣有料的技術文章
做一個積極的人
編碼、改bug、提升自己
我有一個樂園,面向編程,春暖花開!
0. HashMap 簡單說幾句
我們在學習 HashMap 的時候,都知道 HashMap 是非線程安全的,同時我們知道 HashTable 是線程安全的,因為裡面的方法使用了 synchronized 進行同步。
但是 HashMap 為什麼是非線程安全的呢?難道僅僅就是因為內部的方法沒有 synchronized 關鍵字修飾嗎?這篇文章主要來分析一下原因。
我們知道 HashMap 底層是一個 Entry 數組,當發生 hash 衝突的時候,HashMap 是採用鏈表的方式來解決的,在對應的數組位置存放鏈表的頭結點。對鏈表而言,新加入的節點會從頭結點加入。
HashMap為什麼線程不安全,多線程併發的時候在什麼情況下可能出現問題?
Javadoc中關於hashmap的一段描述如下:
此實現不是同步的。如果多個線程同時訪問一個哈希映射,而其中至少一個線程從結構上修改了該映射,則它必須 保持外部同步。(結構上的修改是指添加或刪除一個或多個映射關係的任何操作;僅改變與實例已經包含的鍵關聯的值不是結構上的修改。)這一般通過對自然封裝該映射的對象進行同步操作來完成。如果使用 Collections.synchronizedMap 方法來“包裝”該映射。最好在創建時完成這一操作,以防止對映射進行意外的非同步訪問,如下所示:
Map map = Collections.synchronizedMap(new HashMap<>());
1. HashMap 在插入的時候
在Hashmap做put操作的時候會調用到以上的addEntry方法。
現在假如A線程和B線程同時對同一個數組位置調用addEntry,兩個線程會同時得到現在的頭結點,然後A寫入新的頭結點之後,B也寫入新的頭結點,那B的寫入操作就會覆蓋A的寫入操作造成A的寫入操作丟失。
2. HashMap 在擴容的時候
addEntry中當加入新的鍵值對後鍵值對總數量超過門限值的時候會調用一個resize操作,代碼如下:
這個操作會新生成一個新的容量的數組,然後對原數組的所有鍵值對重新進行計算和寫入新的數組,之後指向新生成的數組。
HashMap 有個擴容的操作,這個操作會新生成一個新的容量的數組,然後對原數組的所有鍵值對重新進行計算和寫入新的數組,之後指向新生成的數組。
那麼問題來了,當多個線程同時進來,檢測到總數量超過門限值的時候就會同時調用 resize 操作,各自生成新的數組並 rehash 後賦給該 map 底層的數組,結果最終只有最後一個線程生成的新數組被賦給該 map 底層,其他線程的均會丟失。而且當某些線程已經完成賦值而其他線程剛開始的時候,就會用已經被賦值的table作為原始數組,這樣也會有問題。
其他地方還有很多可能會出現線程安全問題,我就不一一列舉了,總之 HashMap 是非線程安全的,有併發問題時,建議使用 ConcrrentHashMap。
閱讀更多 Java編程技術樂園 的文章