執行緒安全Map比較
如何執行緒安全的使用HashMap
2016年09月02日 13:37:32
在週二面試時,一面的面試官有問到HashMap是否是執行緒安全的,如何線上程安全的前提下使用HashMap,其實也就是HashMap
,Hashtable
,ConcurrentHashMap
和synchronized Map
的原理和區別。當時有些緊張只是簡單說了下HashMap不是執行緒安全的;Hashtable執行緒安全,但效率低,因為是Hashtable是使用synchronized的,所有執行緒競爭同一把鎖;而ConcurrentHashMap不僅執行緒安全而且效率高,因為它包含一個segment陣列,將資料分段儲存,給每一段資料配一把鎖,也就是所謂的鎖分段技術。當時忘記了synchronized Map和解釋一下HashMap為什麼執行緒不安全。面試結束後問了下面試官哪裡有些不足,面試官說上面這個問題的回答算過關,但可以在深入一些或者自己動手嘗試一下。so~~~雖然拿到了offer,但還是再整理一下,不能得過且過啊。
為什麼HashMap是執行緒不安全的
總說HashMap是執行緒不安全的,不安全的,不安全的,那麼到底為什麼它是執行緒不安全的呢?要回答這個問題就要先來簡單瞭解一下HashMap原始碼中的使用的儲存結構
(這裡引用的是Java 8的原始碼,與7是不一樣的)和它的擴容機制
。
HashMap的內部儲存結構
下面是HashMap使用的儲存結構:
1 2 3 4 5 6 7 8 |
implements Map.Entry<K,V> {
|
可以看到HashMap內部儲存使用了一個Node陣列(預設大小是16),而Node類包含一個型別為Node的next的變數,也就是相當於一個連結串列,所有hash值相同(即產生了衝突)的key會儲存到同一個連結串列裡,大概就是下面圖的樣子(順便推薦個線上畫圖的網站Creately)。
HashMap內部儲存結果
需要注意的是,在Java 8中如果hash值相同的key數量大於指定值(預設是8)時使用平衡樹來代替連結串列,這會將get()方法的效能從O(n)提高到O(logn)。具體的可以看我的另一篇部落格Java 8中HashMap和LinkedHashMap如何解決衝突。
HashMap的自動擴容機制
HashMap內部的Node陣列預設的大小是16,假設有100萬個元素,那麼最好的情況下每個hash桶裡都有62500個元素,這時get(),put(),remove()等方法效率都會降低。為了解決這個問題,HashMap提供了自動擴容機制,當元素個數達到陣列大小loadFactor後會擴大陣列的大小,在預設情況下,陣列大小為16,loadFactor為0.75,也就是說當HashMap中的元素超過16\0.75=12時,會把陣列大小擴充套件為2*16=32,並且重新計算每個元素在新陣列中的位置。如下圖所示(圖片來源,權侵刪)。
自動擴容
從圖中可以看到沒擴容前,獲取EntryE需要遍歷5個元素,擴容之後只需要2次。
為什麼執行緒不安全
個人覺得HashMap在併發時可能出現的問題主要是兩方面,首先如果多個執行緒同時使用put方法新增元素,而且假設正好存在兩個put的key發生了碰撞(hash值一樣),那麼根據HashMap的實現,這兩個key會新增到陣列的同一個位置,這樣最終就會發生其中一個執行緒的put的資料被覆蓋。第二就是如果多個執行緒同時檢測到元素個數超過陣列大小*loadFactor,這樣就會發生多個執行緒同時對Node陣列進行擴容,都在重新計算元素位置以及複製資料,但是最終只有一個執行緒擴容後的陣列會賦給table,也就是說其他執行緒的都會丟失,並且各自執行緒put的資料也丟失。
關於HashMap執行緒不安全這一點,《Java併發程式設計的藝術》一書中是這樣說的:
HashMap在併發執行put操作時會引起死迴圈,導致CPU利用率接近100%。因為多執行緒會導致HashMap的Node連結串列形成環形資料結構,一旦形成環形資料結構,Node的next節點永遠不為空,就會在獲取Node時產生死迴圈。
哇塞,聽上去si不si好神奇,居然會產生死迴圈。。。。google了一下,才知道死迴圈並不是發生在put操作時,而是發生在擴容時。詳細的解釋可以看下面幾篇部落格:
如何執行緒安全的使用HashMap
瞭解了HashMap為什麼執行緒不安全,那現在看看如何執行緒安全的使用HashMap。這個無非就是以下三種方式:
- Hashtable
- ConcurrentHashMap
- Synchronized Map
例子:
1 2 3 4 5 6 7 8 |
|
依次來看看。
Hashtable
先稍微吐槽一下,為啥命名不是HashTable啊,看著好難受,不管了就裝作它叫HashTable吧。這貨已經不常用了,就簡單說說吧。HashTable原始碼中是使用synchronized
來保證執行緒安全的,比如下面的get方法和put方法:
1 2 3 4 5 6 |
|
所以當一個執行緒訪問HashTable的同步方法時,其他執行緒如果也要訪問同步方法,會被阻塞住。舉個例子,當一個執行緒使用put方法時,另一個執行緒不但不可以使用put方法,連get方法都不可以,好霸道啊!!!so~~,效率很低,現在基本不會選擇它了。
ConcurrentHashMap
ConcurrentHashMap(以下簡稱CHM)是JUC包中的一個類,Spring的原始碼中有很多使用CHM的地方。之前已經翻譯過一篇關於ConcurrentHashMap的部落格,如何在java中使用ConcurrentHashMap,裡面介紹了CHM在Java中的實現,CHM的一些重要特性和什麼情況下應該使用CHM。需要注意的是,上面部落格是基於Java 7的,和8有區別,在8中CHM摒棄了Segment(鎖段)的概念,而是啟用了一種全新的方式實現,利用CAS演算法,有時間會重新總結一下。
SynchronizedMap
看了一下原始碼,SynchronizedMap的實現還是很簡單的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
從原始碼中可以看出呼叫synchronizedMap()方法後會返回一個SynchronizedMap類的物件,而在SynchronizedMap類中使用了synchronized同步關鍵字來保證對Map的操作是執行緒安全的。
效能對比
這是要靠資料說話的時代,所以不能只靠嘴說CHM快,它就快了。寫個測試用例,實際的比較一下這三種方式的效率(原始碼來源),下面的程式碼分別通過三種方式建立Map物件,使用ExecutorService
來併發執行5個執行緒,每個執行緒新增/獲取500K個元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
|
測試結果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
這個就不用廢話了,CHM效能是明顯優於Hashtable和SynchronizedMap的,CHM花費的時間比前兩個的一半還少,哈哈,以後再有人問就可以甩資料了。