為什麼 HashMap 是線程不安全的?


前言

為什麼說 HashMap 是線程不安全的,下面,一起學習一下吧。


先上一張解釋線程安全的圖


為什麼 HashMap 是線程不安全的?


圖中 Main 方法中有三個線程,三個線程共享 num 變量,故 num 變量是 static 修飾的,使用 static 修飾後,由於沒有進行原子操作導致,線程 1 在判斷完 num 大小後,時間片被分配到線程 2 ,線程 2 執行完畢後時間片會到線程 1 ,這個時候線程 1 就輸出了錯誤的 num,這是一個很經典的線程安全問題。


再舉一個複雜點的例子,HashMap,所有人知道 HashMap 是線程不安全的,但是恐怕沒幾個人到底為什麼不安全,更沒多少人能證明不安全。


關於 HashMap 線程不安全可以使用以下代碼復現:

<code>final Map<integer> map = new HashMap<>();

final Integer targetKey = 0b1111_1111_1111_1111; // 65 535
final String targetValue = "v";
map.put(targetKey, targetValue);

new Thread(() -> {
IntStream.range(0, targetKey).forEach(key -> map.put(key, "someValue"));
}).start();


while (true) {
if (!targetValue.equals(map.get(targetKey))) {
throw new RuntimeException("HashMap is not thread safe.");
}

}/<integer>/<code>

運行

<code>Exception in thread "main" 
java.lang.RuntimeException: HashMap is not thread safe. at com.example.demo.DMYZDemo.main(DMYZDemo.java:26)/<code>


首先,第5行將 targetKey = targetValue 添加到了 map 中,按道理說這個 targetKey 如果不被刪除,那麼第13行的判斷將會永不成立,但是為什麼拋出了異常呢?


這裡要說下,HashMap 的擴容過程


<code>/**
* Rehashes the contents of this map into a new array with a
* larger capacity. This method is called automatically when the
* number of keys in this map reaches its threshold.
*
* If current capacity is MAXIMUM_CAPACITY, this method does not
* resize the map, but sets threshold to Integer.MAX_VALUE.
* This has the effect of preventing future calls.
*
* @param newCapacity the new capacity, MUST be a power of two;
* must be greater than current capacity unless current
* capacity is MAXIMUM_CAPACITY (in which case value
* is irrelevant).
*/
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {//最大容量為 1 << 30
threshold = Integer.MAX_VALUE;
return;
}

Entry[] newTable = new Entry[newCapacity];//新建一個新表

boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;//是否再hash
transfer(newTable, rehash);//完成舊錶到新表的轉移
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry e : table) {//遍歷同桶數組中的每一個桶
while(null != e) {//順序遍歷某個桶的外掛鏈表
Entry next = e.next;//引用next
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);//找到新表的桶位置;原桶數組中的某個桶上的同一鏈表中的Entry此刻可能被分散到不同的桶中去了,有效的緩解了哈希衝突。
e.next = newTable[i];//頭插法插入新表中
newTable[i] = e;
e = next;
}
}
}
/<code>


對於resize的過程,相對來講是比較簡單清晰易於理解的。舊桶數組中的某個桶的外掛單鏈表是通過頭插法插入新桶數組中的,並且原鏈表中的Entry結點並不一定仍然在新桶數組的同一鏈表。


這裡很容易就想到多線程情況下,隱約感覺這個 transfer 方法在多線程環境下會亂套。事實上也是這樣的,由於缺乏同步機制,當多個線程同時 resize 的時候,某個線程 t 所持有的引用 next(參考上面代碼next指向原桶數組中某個桶外掛單鏈表的下一個需要轉移的 Entry ),可能已經被轉移到了新桶數組中,那麼最後該線程t實際上在對新的桶數組進行 transfer 操作。


如果有更多的線程出現這種情況,那很可能出現大量線程都在對新桶數組進行transfer,那麼就會出現多個線程對同一鏈表無限進行鏈表反轉的操作,極易造成死循環,數據丟失等等,因此 HashMap 不是線程安全的,考慮在多線程環境下使用併發工具包下的 ConcurrentHashMap。


文章推薦一看就會,一寫就廢?詳解遞歸一看就會:最大自序和狀態壓縮算法堅持做一件事,究竟難在哪裡?有趣的多線程和無趣的線程鎖做技術,如何使自己在重複性業務中持續提升?Openresty 配合 redis 實現無感知灰度發佈系統(基礎篇)「純手打」2萬字長文從0開始Spring Boot(上)一次請求 SpringMVC 到底做了什麼?微服務之服務容錯保護( Hystrix 斷路器)SpringCloud 構建微服務架構,分佈式配置中心(加密解密)記一次Spring中HttpMessageConverter的源碼分析


謝謝關注


分享到:


相關文章: