Java容器篇小結之Map自問自答

採用問答的方式對常見的問題進行整理小結

I. Map篇

0. 什麼是Map

看到這個有點懵逼,一時還真不知道怎麼解釋,能讓徹底沒有接觸過的人都能聽懂java

想到生活中一個有意思的場景,和咱們使用Map很是像,拿着新華詞典查字數組

咱們這裏以拼音方式查詢字時,通常步驟以下:安全

  1. 首前後獲取字的拼音
  2. 經過拼音,查詢到字對應的頁碼
  3. 在頁碼中查到對應的字的解釋

再轉換看一下Map的工做原理(主要是HashMap)數據結構

  1. 經過hash()計算key,得出一個hash值(同字轉拼音)多線程

  2. 經過hash值,獲取Node在數組中的索引 (同經過拼音獲取頁碼)併發

  3. 獲取Node,而後遍歷Node#next,查到咱們須要的節點(同在頁碼中找到對應的字)性能

    • 這裏獲取的Node是一個鏈表頭(jdk8中作過優化,可能爲紅黑樹,爲簡單起見,以鏈表說明),若是沒有hash碰撞,這個鏈表就一個節點;若hash碰撞了,則將這些碰撞的節點串成一個鏈表

1. 常見的map有那些

JDK中實現了一些可以覆蓋絕大部分場景的Map容器,羅列一些常見的以下優化

  • HashMap: 主要是利用key的hash來定位對應的元素
  • HashTable
  • EnumMap : key爲枚舉的map,根據枚舉的ordinal做爲定位對應的元素 (用的比較少,後面不歸入分析範疇)
  • TreeMap
  • LinkedHashMap
  • ConcurrentHashMap

2. 根據不一樣的分類方式,對上面的map進行劃分

根據是否線程安全進行分類線程

線程安全 非線程安全
HashTable, ConcurrentHashMap HashMap, TreeMap, LinkedHashMap

根據Map是否有序進行分類設計

有序 無序
TreeMap, LinkedHashMap HashMap, ConcurrentHashMap, HashTable

根據key怎麼獲取對應的元素

hash定位 其餘定位
HashTable, ConcurrentHashMap, HashMap, LinkedHashMap TreeMap

TreeMap比較有意思,要求指定一個比較器,或者key可自比較,並且若是塞入兩個不一樣的kv對,可是key經過比較器發現相等時,會用後入的kv對中的value替換前面的那個,即定位是根據比較器來的


根據底層數據結構進行分類

數組+鏈表
HashTable, ConcurrentHashMap, HashMap, LinkedHashMap TreeMap

3. HashMap怎麼用,如何實現的

>>> 如何使用

最最多見的使用方式,三把斧便可,以下

// 1. 建立一個Map對象
Map<String, String> map = new HashMap<>();

// 2. 塞入kv數據對
map.put("key", "value");

// 3. 取出key對應的數據
String value = map.get("key");

其次就是使用的注意事項

  • Map中key和value均可覺得null,可是如若不是需求場景中,必需要塞null,不然就不要這麼幹;由於使用時,若是漏了null判斷,很是容易產生npe
  • 若是能知道這個Map中大概會存多少數據,就在初始化時,指定容量(避免頻繁的擴容,致使的性能開銷)
  • 非線程安全(如須要線程安全,採用ConcurrentHashMap,不要用HashTable
  • 無序(如須要有序,採用TreeMap, LinkedHashMap,實際後者用得更多)

此外分享一個實際項目中關於HashMap的一個優化點

通常來講,HashMap的get(key)方法是O(1)時間開銷,可是因爲獲取對應value,會頻繁的計算hash值,且不可避免的會產生Hash碰撞,這些都是會有額外的開銷(cpu和時間開銷)

咱們的一個應用中,存在大量的配置開關(用與各類預案,各類場景的切換)存在一個大的HashMap中,致使每次提供服務時,都會去這個Map中屢次查詢Map中的配置值,咱們作的優化是將Map映射到一個配置類,以此減小頻繁的hash操做

遺憾的是最後性能提高並非特別明顯,也就1-2毫秒的樣子...(若是系統的rt要求特別嚴格的能夠考慮從這個方面出發)


>>> 如何實現的

簡單來說,從兩個點出發,一是數據結構,二是如何向其中添加和取數據

底層存儲結構以下圖:

數組+鏈表(or紅黑樹),數組的容量,必然爲2的n次方


讀寫數據:

  • 經過hash方法獲取key對應的hash值
  • hash值對數組長度取模,即爲kv可能在數組中出現位置
  • 獲取數組中對應索引的元素Node,判斷是否爲咱們須要的
    • 判斷規則:hash值相同,key相同,or equals()判斷相等
    • 若Node不知足,則判斷Node#next對應的Node節點
    • 直到找到匹配的值,or壓根就木有時,才返回

另一點就是擴容

  • 當塞入一個數據後,Map中元素的個數大於 capacity * loadFactor (通常是容量*0.75),則數組會出現擴容,擴爲原來的兩倍
  • 擴容後,原來的Node節點可能會向後移(新的索引爲hash值對新的容量取模)

4. HashTable和ConcurrentHashMap的安全保證是怎樣的,有什麼區別

二者都是線程安全的,但底層的實現原理確實徹底不一樣

  • HashTable
    • 全部方法上加上 synchronized 關鍵字,實行加鎖同步
  • ConcurrentHashMap
    • 寫使用分段鎖機制,把整個哈希表切分紅段segment(默認爲16段),每段有一個鎖,最多能夠同時有16個寫線程。而讀不受限制

5. HashMap是否有序,如何保證有序

HashMap無序,但實際的業務場景中,須要有序的地方還很多,通常來將,常見的順序要求是根據前後塞入Map容器的順序來肯定,此時能夠考慮採用 LinkedHashMap,確保先塞入Map的,在遍歷時,優先出來

若是有比較複雜的排序場景,則能夠採用TreeMap,使用的時候須要額外注意一些使用事項

6. 幾種遍歷方式

通常遍歷就是下面三中場景了

// 遍歷kv
for(Map.Entry<String, String> entry: map.entrySet()) {
// ....
}

// 遍歷key值
for(Object key : map.keySet()) {
  // xxx
}

// 遍歷value值
for(Object value: map.values()) {
  // xxxx
}

根據不一樣的場景選擇遍歷方式

  • 若是須要kv,則遍歷EntrySet
  • 若是隻須要key, 則遍歷 KeySet
  • 若是隻須要value,則遍歷 ValueSet

上面的遍歷過程當中,都是不容許對Map進行增刪操做的,不然會拋一個併發修改異常;若是在遍歷過程當中,須要根據對應的值,作一些處理,採用迭代器方式, 一個demo以下

Map<String, String> map = new HashMap<>();
// ....
Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
    Map.Entry<String, String> entry = iterator.next();
    iterator.remove();
    iterator.next();
}

7. 如何設計一個線程安全的HashMap

蛋疼的問題,真要本身來設計的話,最簡單的就是HashTable這種全加鎖的機制;可是這種實際是強制使多線程串行工做了,若是須要併發工做呢?

除了ConcurrentHashMap的鎖分段機制,感受能夠參考CopyOnWriteArrayList的實現方式,來一個CopyOnWriteHashMap,對寫進行加鎖,讀無鎖,具體的實現,有待完善...

Java分享,關注小灰灰Blog

相關文章
相關標籤/搜索