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中相關問題的博客:

 

 

ConcurrentHashMap的參考博客:

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM