1. 程式人生 > 實用技巧 >Map的常用所有實現類資料結構簡單解析

Map的常用所有實現類資料結構簡單解析

一、Map集合框架

HashTable執行緒安全。 Properties是配置檔案讀取使用。 HashMap基於散列表的實現,插入和查詢的鍵值對的開銷是固定的, LindkedHashMap類似於HashMap,插入時有次序,插入時略慢,但是基於連結串列的遍歷叫較快。 TreeMap基於紅黑樹的實現,他們會被排序,它是唯一帶有subMap方法的Map,它可以返回一個子樹。 ConcurrentHashMap一種執行緒安全的map,它不涉及同步加鎖。 LinkedHashMap可以使用最近最少使用演算法(LRU)演算法,最近沒有被使用的元素會排在前面。

一、HashMap

JDK1.7和JDK1.8,區別還是蠻大的。

JDK1.8使用的是陣列+連結串列+紅黑樹

比如:

Map<String,Object> map = new HashMap<String,Object>();

然後我們put進去一個值。

map.put("name","caesar");

然後它是怎末操作的呢。

首先說,如果不指定陣列的大小,預設陣列大小的長度是16位。

然後算出name的hashCode,通過HashCode計算出,該資料因該位於陣列中的位置。

先進行異或操作,使hashCode更加的隨機。

然後使用與操作,來獲取陣列的位置,然後判斷是否重複,不重複直接新增,重複就判斷是連結串列還是紅黑樹,然後使用不同的方式來進行新增操作。

有如下幾個疑問:

1、為什麼要使用與運算來計算陣列的位置,為什麼不適用模運算呢?

很顯然,與運算快呀,模運算要進行三步操作:除法,乘法,減法

2、為什麼陣列的長度最好為2的倍數如果不是2的倍數,要改為距離最近的2的倍數,演算法如下:

假如說陣列長度為16,那麼與的樹為15,也就是1111,這時候是沒有空間浪費的而,當陣列長度為15的時候,hashcode的值會與14(1110)進行“與”,那麼最後一位永遠是0,而0001,0011,0101,1001,1011,0111,1101這幾個位置永遠都不能存放元素了,空間浪費相當大,更糟的是這種情況中,陣列可以使用的位置比陣列長度小了很多,這意味著進一步增加了碰撞的機率,減慢了查詢的效率!

3、如果hashCode不同,就不存在hash碰撞嗎?

當然不是啦,碰撞是因為,hashCode和length-1與完的值,是有可能形相同的。

4、hashmap的resize怎樣改善效率

當hashmap中的元素越來越多的時候,碰撞的機率也就越來越高(因為陣列的長度是固定的),所以為了提高查詢的效率,就要對hashmap的陣列進行擴容,陣列擴容這個操作也會出現在ArrayList中,所以這是一個通用的操作,很多人對它的效能表示過懷疑,不過想想我們的“均攤”原理,就釋然了,而在hashmap陣列擴容之後,最消耗效能的點就出現了:原陣列中的資料必須重新計算其在新陣列中的位置,並放進去,這就是resize。

那麼hashmap什麼時候進行擴容呢?當hashmap中的元素個數超過陣列大小*loadFactor時,就會進行陣列擴容,loadFactor的預設值為0.75,也就是說,預設情況下,陣列大小為16,那麼當hashmap中元素個數超過16*0.75=12的時候,就把陣列的大小擴充套件為2*16=32,即擴大一倍,然後重新計算每個元素在陣列中的位置,而這是一個非常消耗效能的操作,所以如果我們已經預知hashmap中元素的個數,那麼預設元素的個數能夠有效的提高hashmap的效能。比如說,我們有1000個元素new HashMap(1000), 但是理論上來講new HashMap(1024)更合適,不過上面annegu已經說過,即使是1000,hashmap也自動會將其設定為1024。 但是new HashMap(1024)還不是更合適的,因為0.75*1000 < 1000, 也就是說為了讓0.75 * size > 1000, 我們必須這樣new HashMap(2048)才最合適,既考慮了&的問題,也避免了resize的問題。

5、為什麼不一開始就使用紅黑樹,要等長度到了8才使用?

  • HashMap 解決 hash 衝突的時候,先用連結串列,再轉紅黑樹,是為了時間和空間的平衡。
  • TreeNodes 佔用的空間大小大約是普通 Nodes 的兩倍,只有在容器中包含足夠的節點保證使用才用它,在節點數比較小的時候,對於紅黑樹來說,記憶體上的劣勢會超過查詢等操作的優勢,使用連結串列更加好。
  • 節點數比較多的時候,綜合考慮時間和空間,紅黑樹比連結串列要好

JDK1.7的資料結構是陣列+連結串列。不多說。

其實紅黑樹在陣列量小的時候是不會用到了,據統計使用到紅黑樹的概率是千萬分之一。

二、LinkedHashMap

實際和HashMap類似,就是多了兩個指標而已,可以維護插入順序,但是插入時較慢。

三、TreeMap

TreeMap使用的資料結構是紅黑樹,能到的查詢效率為logn,主要是可以根據自定義的排序方法進行排序,主要還是排序時使用。

四、HashTable

synchronized來實現鎖,實現執行緒安全,效率低。

五、ConcurrentHashMap

這個執行緒安全的Map,對效能進行了優化。

JDK1.7和1.8也是有較大的區別。

先說1.7吧

1.7拋棄了全synchronized,效率太低,使用分段鎖(實際結構為:陣列+陣列+連結串列)

HashTable容器在競爭激烈的併發環境下表現出效率低下的原因,是因為所有訪問HashTable的執行緒都必須競爭同一把鎖,那假如容器裡有多把鎖,每一把鎖用於鎖容器其中一部分資料,那麼當多執行緒訪問容器裡不同資料段的資料時,執行緒間就不會存在鎖競爭,從而可以有效的提高併發訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術,首先將資料分成一段一段的儲存,然後給每一段資料配一把鎖,當一個執行緒佔用鎖訪問其中一個段資料的時候,其他段的資料也能被其他執行緒訪問。

主要結構:

ConcurrentHashMap是由Segment陣列結構和HashEntry陣列結構組成。Segment是一種可重入鎖ReentrantLock,在ConcurrentHashMap裡扮演鎖的角色,HashEntry則用於儲存鍵值對資料。一個ConcurrentHashMap裡包含一個Segment陣列,Segment的結構和HashMap類似,是一種陣列和連結串列結構, 一個Segment裡包含一個HashEntry陣列,每個HashEntry是一個連結串列結構的元素, 每個Segment守護者一個HashEntry數組裡的元素,當對HashEntry陣列的資料進行修改時,必須首先獲得它對應的Segment鎖。

Segment 繼承於 ReentrantLock,呼叫,Lock()+UnLock()的方法,來進行加鎖,解鎖,

1、我認為最妙的在於,它這個Segment通過HashCode來進行的陣列定位。

hash >>> segmentShift) & segmentMask

segmentShift和segmentMask。這兩個全域性變數在定位segment時的雜湊演算法裡需要使用,sshift等於ssize從1向左移位的次數,在預設情況下concurrencyLevel等於16,1需要向左移位移動4次,所以sshift等於4。segmentShift用於定位參與hash運算的位數,segmentShift等於32減sshift,所以等於28,這裡之所以用32是因為ConcurrentHashMap裡的hash()方法輸出的最大數是32位的,後面的測試中我們可以看到這點。segmentMask是雜湊運算的掩碼,等於ssize減1,即15,掩碼的二進位制各個位的值都是1。因為ssize的最大長度是65536,所以segmentShift最大值是16,segmentMask最大值是65535,對應的二進位制是16位,每個位都是1。

segmentMask實際就是length-1,不多說

無符號右移,是前面的各項全部為0,比如segment陣列有16位,2的四次方,那麼hashCode32位,右移動(32-4),也就是28位,之後,就剩下了4位,正好和segmentMask取與,高位運算,實際上直接與也可以,但是出現hash衝突的概率加大。

2、還有就是get不加鎖:

get操作的高效之處在於整個get過程不需要加鎖,除非讀到的值是空的才會加鎖重讀,我們知道HashTable容器的get方法是需要加鎖的,那麼ConcurrentHashMap的get操作是如何做到不加鎖的呢?原因是它的get方法裡將要使用的共享變數都定義成volatile,如用於統計當前Segement大小的count欄位和用於儲存值的HashEntry的value。定義成volatile的變數,能夠線上程之間保持可見性,能夠被多執行緒同時讀,並且保證不會讀到過期的值,但是隻能被單執行緒寫(有一種情況可以被多執行緒寫,就是寫入的值不依賴於原值),在get操作裡只需要讀不需要寫共享變數count和value,所以可以不用加鎖。之所以不會讀到過期的值,是根據java記憶體模型的happen before原則,對volatile欄位的寫入操作先於讀操作,即使兩個執行緒同時修改和獲取volatile變數,get操作也能拿到最新的值,這是用volatile替換鎖的經典應用場景。

然後聊聊JDK1.8的升級版本

結構還是陣列+連結串列+紅黑樹

1.8 在 1.7 的資料結構上做了大的改動,採用紅黑樹之後可以保證查詢效率(O(logn)),甚至取消了 ReentrantLock 改為了 synchronized,這樣可以看出在新版的 JDK 中對 synchronized 優化是很到位的。因為synchronized實際是有鎖升級過程的,可以看我的另一篇部落格。

https://www.cnblogs.com/mcjhcnblogs/p/14226505.html

其中一些不太懂得部分,也參考了其他大牛的部落格,講的比較清楚

參考部落格:

HashMap中相關問題的部落格:

https://www.iteye.com/topic/539465 https://blog.csdn.net/weixin_43093006/article/details/111469521

ConcurrentHashMap的參考部落格:

https://blog.csdn.net/yansong_8686/article/details/50664351 https://blog.csdn.net/weixin_44460333/article/details/86770169