為什么HashMap是線程不安全的
總說 HashMap 是線程不安全的,不安全的,不安全的,那么到底為什么它是線程不安全的呢?要回答這個問題就要先來簡單了解一下 HashMap 源碼中的使用的存儲結構
(這里引用的是 Java 8 的源碼,與7是不一樣的)和它的擴容機制
。
HashMap 內部存儲使用了一個 Node 數組(默認大小是16),而 Node 類包含一個類型為 Node 的 next 的變量,也就是相當於一個鏈表,所有根據 hash 值計算的 bucket 一樣的 key 會存儲到同一個鏈表里(即產生了沖突)。
HashMap的自動擴容機制
HashMap 內部的 Node 數組默認的大小是16,假設有100萬個元素,那么最好的情況下每個 hash 桶里都有62500個元素,這時get(),put(),remove()等方法效率都會降低。為了解決這個問題,HashMap 提供了自動擴容機制,當元素個數達到數組大小 loadFactor 后會擴大數組的大小,在默認情況下,數組大小為16,loadFactor 為0.75,也就是說當 HashMap 中的元素超過16\0.75=12時,會把數組大小擴展為2*16=32,並且重新計算每個元素在新數組中的位置。
為什么線程不安全
個人覺得 HashMap 在並發時可能出現的問題主要是兩方面,首先如果多個線程同時使用put方法添加元素,而且假設正好存在兩個 put 的 key 發生了碰撞(根據 hash 值計算的 bucket 一樣),那么根據 HashMap 的實現,這兩個 key 會添加到數組的同一個位置,這樣最終就會發生其中一個線程的 put 的數據被覆蓋。第二就是如果多個線程同時檢測到元素個數超過數組大小* loadFactor ,這樣就會發生多個線程同時對 Node 數組進行擴容,都在重新計算元素位置以及復制數據,但是最終只有一個線程擴容后的數組會賦給 table,也就是說其他線程的都會丟失,並且各自線程 put 的數據也丟失。
《Java並發編程的藝術》一書中是這樣說的:HashMap 在並發執行 put 操作時會引起死循環,導致 CPU 利用率接近100%。因為多線程會導致 HashMap 的 Node 鏈表形成環形數據結構,一旦形成環形數據結構,Node 的 next 節點永遠不為空,就會在獲取 Node 時產生死循環。
死循環並不是發生在 put 操作時,而是發生在擴容時。
如何線程安全的使用HashMap
了解了 HashMap 為什么線程不安全,那現在看看如何線程安全的使用 HashMap。這個無非就是以下三種方式:
-
Hashtable
HashTable的get/put方法都被synchronized關鍵字修飾,說明它們是方法級別阻塞的,它們占用共享資源鎖,所以導致同時只能一個線程操作get或者put,而且get/put操作不能同時執行,所以這種同步的集合效率非常低,一般不建議使用這個集合。
-
ConcurrentHashMap
private Map<String, Object> map = new ConcurrentHashMap<>();
這個也是最推薦使用的線程安全的Map,也是實現方式最復雜的一個集合,每個版本的實現方式也不一樣,在jdk8之前是使用分段加鎖的一個方式,分成16個桶,每次只加鎖其中一個桶,而在jdk8又加入了紅黑樹和CAS算法來實現。
-
Synchronized Map
private Map<String, Object> map = Collections.synchronizedMap(new HashMap<String, Object>());
這種是直接使用工具類里面的方法創建SynchronizedMap