上一篇 List 踩坑文章中,咱們提到幾個比較容易踩坑的點。做爲 List 集合好兄弟 Map,咱們也是每天都在使用,一不當心也會踩坑。html
今天我就來總結這些常見的坑,再撈本身一手,防止後續同窗再繼續踩坑。安全
本文設計知識點以下:多線程
這個踩坑經歷仍是發生在實習的時候,那時候有這樣一段業務代碼,功能很簡單,從 XML 中讀取相關配置,存入 Map 中。併發
代碼示例以下:ide
那時候正好有個小需求,須要改動一下這段業務代碼。改動的過程當中,忽然想到 HashMap
併發過程可能致使死鎖的問題。性能
因而改動了一下這段代碼,將 HashMap
修改爲了 ConcurrentHashMap
。線程
美滋滋提交了代碼,而後當天上線的時候,就發現炸了。。。設計
應用啓動過程發生 NPE 問題,致使應用啓動失敗。3d
根據異常日誌,很快就定位到了問題緣由。因爲 XML 某一項配置問題,致使讀取元素爲 null,而後元素置入到 ConcurrentHashMap
中,拋出了空指針異常。指針
這不科學啊!以前 HashMap
都沒問題,均可以存在 null,爲何它老弟 ConcurrentHashMap
就不能夠?
翻閱了一下 ConcurrentHashMap#put
方法的源碼,開頭就看到了對 KV 的判空校驗。
看到這裏,不知道你有沒有疑惑,爲何 ConcurrentHashMap
與 HashMap
設計的判斷邏輯不同?
求助了下萬能的 Google,找到 Doug Lea 老爺子的回答:
來源:http://cs.oswego.edu/pipermail/concurrency-interest/2006-May/002485.html
總結一下:
上面提到 Josh Bloch 正是 HashMap
做者,他與 Doug Lea 在 null 問題意見並不一致。
也許正是由於這些緣由,從而致使 ConcurrentHashMap
與 HashMap
對於 null 處理並不同。
最後貼一下經常使用 Map 子類集合對於 null 存儲狀況:
上面的實現類約束,都太不同,有點很差記憶。其實只要咱們在加入元素以前,主動去作空指針判斷,不要在 Map 中存入 null,就能夠從容避免上面問題。
先來看個簡單的例子,咱們自定義一個 Goods
商品類,將其做爲 Key 存在 Map 中。
示例代碼以下:
上面代碼中,第二次咱們加入一個相同的商品,本來咱們指望新加入的值將會替換原來舊值。可是實際上這裏並無替換成功,反而又加入一對鍵值。
翻看一下 HashMap#put
的源碼:
如下代碼基於 JDK1.7
這裏首先判斷 hashCode
計算產生的 hash,若是相等,再判斷 equals
的結果。可是因爲 Goods
對象未重寫的hashCode
與 equals
方法,默認狀況下 hashCode
將會使用父類對象 Object 方法邏輯。
而 Object#hashCode
是一個 native 方法,默認將會爲每個對象生成不一樣 hashcode(與內存地址有關),這就致使上面的狀況。
因此若是須要使用自定義對象作爲 Map 集合的 key,那麼必定記得重寫hashCode
與 equals
方法。
而後當你爲自定義對象重寫上面兩個方法,接下去又可能踩坑另一個坑。
使用 lombok 的
EqualsAndHashCode
自動重寫hashCode
與equals
方法。
上面的代碼中,當 Map 中置入自定義對象後,接着修改了商品金額。而後當咱們想根據同一個對象取出 Map 中存的值時,卻發現取不出來了。
上面的問題主要是由於 get
方法是根據對象 的 hashcode 計算產生的 hash 值取定位內部存儲位置。
當咱們修改了金額字段後,致使 Goods
對象 hashcode 產生的了變化,從而致使 get 方法沒法獲取到值。
經過上面兩種狀況,能夠看到使用自定義對象做爲 Map 集合 key,仍是挺容易踩坑的。
因此儘可能避免使用自定義對象做爲 Map 集合 key,若是必定要使用,記得重寫 hashCode
與 equals
方法。另外還要保證這是一個不可變對象,即對象建立以後,沒法再修改裏面字段值。
以前的文章『天天都在用 Map,這些核心技術你知道嗎?』咱們說過 HashMap
是一個線程不安全的容器,多線程環境爲了線程安全,咱們須要使用 ConcurrentHashMap
代替。
可是不要認爲使用了 ConcurrentHashMap
必定就能保證線程安全,在某些錯誤的使用場景下,依然會形成線程不安全。
上面示例代碼,咱們本來指望輸出 1001,可是運行幾回,獲得結果都是小於 1001。
深刻分析這個問題緣由,其實是由於第一步與第二步是一個組合邏輯,不是一個原子操做。
ConcurrentHashMap
只能保證這兩步單的操做是個原子操做,線程安全。可是並不能保證兩個組合邏輯線程安全,頗有可能 A 線程剛經過 get 方法取到值,還將來得及加 1,線程發生了切換,B 線程也進來取到一樣的值。
這個問題一樣也發生在其餘線程安全的容器,好比 Vector
等。
上面的問題解決辦法也很簡單,加鎖就能夠解決,不過這樣就會使性能大打折扣,因此不太推薦。
咱們可使用 AtomicInteger
解決以上的問題。
上一篇文章中咱們提過,Arrays#asList
與 List#subList
返回 List 將會與原集合互相影響,且可能並不支持 add
等方法。一樣的,這些坑爹的特性在 Map 中也存在,一不當心,將會再次掉坑。
Map 接口除了支持增刪改查功能之外,還有三個特有的方法,能返回全部 key,返回全部的 value,返回全部 kv 鍵值對。
// 返回 key 的 set 視圖 Set<K> keySet(); // 返回全部 value Collection 視圖 Collection<V> values(); // 返回 key-value 的 set 視圖 Set<Map.Entry<K, V>> entrySet();
這三個方法建立返回新集合,底層其實都依賴的原有 Map 中數據,因此一旦 Map 中元素變更,就會同步影響返回的集合。
另外這三個方法返回新集合,是不支持的新增以及修改操做的,可是卻支持 clear、remove
等操做。
示例代碼以下:
因此若是須要對外返回 Map 這三個方法產生的集合,建議再來個套娃。
new ArrayList<>(map.values());
最後再簡單提一下,使用 foreach
方式遍歷新增/刪除 Map 中元素,也將會和 List 集合同樣,拋出 ConcurrentModificationException
。
從上面文章能夠看到不論是 List 提供的方法返回集合,仍是 Map 中方法返回集合,底層實際仍是使用原有集合的元素,這就致使二者將會被互相影響。因此若是須要對外返回,請使用套娃大法,這樣讓別人用的也安心。
第二, Map 各個實現類對於 null 的約束都不太同樣,這裏建議在 Map 中加入元素以前,主動進行空指針判斷,提早發現問題。
第三,慎用自定義對象做爲 Map 中的 key,若是須要使用,必定要重寫 hashCode
與 equals
方法,而且還要保證這是個不可變對象。
第三,ConcurrentHashMap
是線程安全的容器,可是不要思惟定勢,不要片面認爲使用 ConcurrentHashMap
就會線程安全。