你們好,我是Java最全面試題庫
的提褲姐,今天這篇是JavaSE系列
的第十篇,主要總結了Java集合中的Map集合
,在後續,會沿着第一篇開篇的知識線路一直總結下去,作到日更!若是我能作到百日百更,但願你也能夠跟着百日百刷,一百天養成一個好習慣。java
須要重寫equals()
和hashCode()
方法。面試
1.HashMap存儲鍵值對實現快速存取,容許爲null。key值不可重複,若key值重複則覆蓋。
2.非同步,線程不安全。
3.底層是hash表,不保證有序(好比插入的順序);算法
1.7版本:數組
+鏈表
1.8版本:數組
+鏈表
+紅黑樹
數組
圖中,紫色部分即表明哈希表,也稱爲哈希數組(默認數組大小是16,每對key-value鍵值對實際上是存在map的內部類entry裏的),數組的每一個元素都是一個單鏈表的頭節點,跟着的綠色鏈表是用來解決衝突的,若是不一樣的key映射到了數組的同一位置處,就會採用頭插法將其放入單鏈表中。安全
什麼時候進行擴容?多線程
HashMap使用的是懶加載,構造完HashMap對象後,只要不進行put 方法插入元素以前,HashMap並不會去初始化或者擴容table。
當首次調用put方法時,HashMap會發現table爲空而後調用resize方法進行初始化,當添加完元素後,若是HashMap發現size(元素總數)大於threshold(閾值),則會調用resize方法進行擴容。併發
擴容過程:
若threshold(閾值)不爲空,table的首次初始化大小爲閾值,不然初始化爲缺省值大小16
默認的負載因子大小爲0.75
,當一個map填滿了75%的bucket時候(即達到了默認負載因子0.75),就會擴容,擴容後的table大小變爲原來的兩倍(擴容後自動計算每一個鍵值對位置,且長度必須爲16或者2的整數次冪)
若不是16或者2的冪次,位運算的結果不夠均勻分佈,顯然不符合Hash算法均勻分佈的原則。
反觀長度16或者其餘2的冪,Length-1
的值是全部二進制位全爲1,這種狀況下,index的結果等同於HashCode後幾位的值。只要輸入的HashCode自己分佈均勻,Hash算法的結果就是均勻的。
假設擴容前的table大小爲2的N次方,元素的table索引爲其hash值的後N位肯定擴容後的table大小即爲2的N+1次方,則其中元素的table索引爲其hash值的後N+1位肯定,比原來多了一位從新調整map的大小,並將原來的對象放入新的bucket數組中。這個過程叫做rehashing
所以,table中的元素只有兩種狀況:
元素hash值第N+1位爲0:不須要進行位置調整
元素hash值第N+1位爲1:將當前位置移動到 原索引+未擴容前的數組長度
的位置
擴容或初始化完成後,resize方法返回新的table性能
public static void main(String[] args) { Map<String, String> map = new HashMap<>(); //添加方法 map.put("個人暱稱", "極多人小紅"); map.put("個人csdn", "csdn_hcx"); map.put("個人簡書", "js_hcx"); map.put("個人網站", "www.hcxblog.site"); //map集合中遍歷方式一: 使用keySet方法進行遍歷 缺點:keySet方法只是返回了全部的鍵,沒有值。 Set<String> keys = map.keySet(); //keySet() 把Map集合中的全部鍵都保存到一個Set類型 的集合對象中返回。 Iterator<String> it = keys.iterator(); while (it.hasNext()) { String key = it.next(); System.out.println("鍵:" + key + " 值:" + map.get(key)); } //map集合的遍歷方式二: 使用values方法進行 遍歷。 缺點:values方法只能返回全部 的值,沒有鍵。 Collection<String> c = map.values(); //values() 把全部的值存儲到一個Collection集合中返回。 Iterator<String> it2 = c.iterator(); while (it2.hasNext()) { System.out.println("值:" + it2.next()); } //map集合的遍歷方式三:entrySet方法遍歷。 Set<Map.Entry<String, String>> entrys = map.entrySet(); //由於Iterator遍歷的是每個entry,因此也用泛型:<Map.Entry<String,String>> Iterator<Map.Entry<String, String>> it3 = entrys.iterator(); while (it3.hasNext()) { Map.Entry<String, String> entry = it3.next(); System.out.println("鍵:" + entry.getKey() + " 值:" + entry.getValue()); } }
併發集合常見的有 ConcurrentHashMap
、ConcurrentLinkedQueue
、ConcurrentLinkedDeque
等。併發集合位於 java.util.concurrent 包下,是 jdk1.5 以後纔有的。
在 java 中有普通集合、同步(線程安全)的集合、併發集合。
普通集合一般性能最高,可是不保證多線程的安全性和併發的可靠性。
線程安全集合僅僅是給集合添加了 synchronized 同步鎖,嚴重犧牲了性能,並且對併發的效率就更低了,併發集合則經過複雜的策略不只保證了多線程的安全又提升的併發時的效率。優化
put()原理:
1.根據key獲取對應hash值:int hash = hash(key.hash.hashcode())
2.根據hash值和數組長度肯定對應數組引int i = indexFor(hash, table.length);
簡單理解就是i = hash值%模以 數組長度
(實際上是按位與運算)。若是不一樣的key都映射到了數組的同一位置處,就將其放入單鏈表中。且新來的是放在頭節點。網站
get()原理:
經過hash得到對應數組位置,遍歷該數組所在鏈表(key.equals())
採用「頭插法
」,放到對應的鏈表的頭部。
由於HashMap的發明者認爲,後插入的Entry被查找的可能性更大,因此放在頭部。(由於get()查詢的時候會遍歷整個鏈表)。
不是,由於沒加鎖。
hashmap在接近臨界點時,若此時兩個或者多個線程進行put操做,都會進行resize(擴容)和ReHash(爲key從新計算所在位置),而ReHash在併發的狀況下可能會造成鏈表環。在執行get的時候,會觸發死循環,引發CPU的100%問題。
注:jdk8已經修復hashmap這個問題了,jdk8中擴容時保持了原來鏈表中的順序。可是HashMap還是非併發安全,在併發下,仍是要使用
ConcurrentHashMap
。
建立兩個指針A和B(在java裏就是兩個對象引用),同時指向這個鏈表的頭節點。而後開始一個大循環,在循環體中,讓指針A每次向下移動一個節點,讓指針B每次向下移動兩個節點,而後比較兩個指針指向的節點是否相同。若是相同,則判斷出鏈表有環,若是不一樣,則繼續下一次循環。
通俗易懂一點:在一個環形跑道上,兩個運動員在同一地點起跑,一個運動員速度快,一個運動員速度慢。當兩人跑了一段時間,速度快的運動員必然會從速度慢的運動員身後再次追上並超過,緣由很簡單,由於跑道是環形的。
結構:
hashmap是由entry數組組成,而ConcurrentHashMap則是Segment數組組成。
Segment自己就至關於一個HashMap。
同HashMap同樣,Segment包含一個HashEntry數組,數組中的每個HashEntry既是一個鍵值對,也是一個鏈表的頭節點。
單一的Segment結構以下:
Segment對象在ConcurrentHashMap集合中有2的N次方個,共同保存在一個名爲segments的數組當中。
能夠說,ConcurrentHashMap是一個二級哈希表。在一個總的哈希表下面,有若干個子哈希表。(這樣類比理解多個hashmap組成一個cmap)
put()原理:
1.爲輸入的Key作Hash運算,獲得hash值。
2.經過hash值,定位到對應的Segment對象
3.獲取可重入鎖
4.再次經過hash值,定位到Segment當中數組的具體位置。
5.插入或覆蓋HashEntry對象。
6.釋放鎖。
get()原理:
1.爲輸入的Key作Hash運算,獲得hash值。
2.經過hash值,定位到對應的Segment對象
3.再次經過hash值,定位到Segment當中數組的具體位置。
因而可知,和hashmap相比,ConcurrentHashMap在讀寫的時候都須要進行二次定位。先定位到Segment,再定位到Segment內的具體數組下標。
由於前者是用的分段鎖,根據hash值鎖住對應Segment對象,當hash值不一樣時,使其能實現並行插入,效率更高,而hashtable則會鎖住整個map。
並行插入:當cmap須要put元素的時候,並非對整個map進行加鎖,而是先經過hashcode來知道他要放在那一個分段(Segment對象)中,而後對這個分段進行加鎖,因此當多線程put的時候,只要不是放在同一個分段中,就實現了真正的並行的插入。
注意:在統計size的時候,就是獲取ConcurrentHashMap全局信息的時候,就須要獲取全部的分段鎖才能統計(即效率稍低)。
分段鎖設計解決的問題:
目的是細化鎖的粒度,當操做不須要更新整個數組的時候,就僅僅針對數組中的一部分行加鎖操做。
HashMap是支持null鍵和null值,而ConcurrentHashMap卻不支持
查看源碼以下:
HashMap:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
ConcurrentHashMap:
/** Implementation for put and putIfAbsent */ final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; ...... }
緣由:經過get(k)獲取對應的value時,若是獲取到的是null,此時沒法判斷它是put(k,v)的時候value爲null,仍是這個key歷來沒有作過映射(即沒有找到這個key)。而HashMap是非併發的,能夠經過contains(key)來作這個判斷。而支持併發的Map在調用m.contains(key)和m.get(key),m可能已經不一樣了。
1.爲了加快查詢效率,java8的HashMap引入了紅黑樹結構
,當數組長度大於默認閾值64
時,且當某一鏈表
的元素>8時,該鏈表就會轉成紅黑樹結構,查詢效率更高。
2.優化擴容方法,在擴容時保持了原來鏈表中的順序,避免出現死循環
紅黑樹:一種自平衡二叉樹,擁有優秀的查詢和插入/刪除性能,普遍應用於關聯數組。對比AVL樹,AVL要求每一個結點的左右子樹的高度之差的絕對值(平衡因子)最多爲1,而紅黑樹經過適當的放低該條件(紅黑樹限制從根到葉子的最長的可能路徑很少於最短的可能路徑的兩倍長,結果是這個樹大體上是平衡的),以此來減小插入/刪除時的平衡調整耗時,從而獲取更好的性能,而這雖然會致使紅黑樹的查詢會比AVL稍慢,但相比插入/刪除時獲取的時間,這個付出在大多數狀況下顯然是值得的。
1.8的實現已經拋棄了Segment分段鎖機制,利用Node數組
+CAS
+Synchronized
來保證併發更新的安全,底層採用數組+鏈表+紅黑樹
的存儲結構。
CAS:
CAS,全稱Compare And Swap(比較與交換),解決多線程並行狀況下使用鎖形成性能損耗的一種機制。java.util.concurrent包中大量使用了CAS原理。
JDK1.8 中的CAS:
Unsafe類,在sun.misc包下,不屬於Java標準。Unsafe類提供一系列增長Java語言能力的操做,如內存管理、操做類/對象/變量、多線程同步等。其中與CAS相關的方法有如下幾個:
//var1爲CAS操做的對象,offset爲var1某個屬性的地址偏移值,expected爲指望值,var2爲要設置的值,利用JNI來完成CPU指令的操做 public final native boolean compareAndSwapObject(Object var1, long offset, Object expected, Object var2); public final native boolean compareAndSwapInt(Object var1, long offset, int expected, int var2); public final native boolean compareAndSwapLong(Object var1, long offset, long expected, long var2);
CAS缺點:
ABA問題
。當第一個線程執行CAS操做,還沒有修改成新值以前,內存中的值已經被其餘線程連續修改了兩次,使得變量值經歷 A->B->A 的過程。解決方案:添加版本號做爲標識,每次修改變量值時,對應增長版本號;作CAS操做前須要校驗版本號。JDK1.5以後,新增AtomicStampedReference類來處理這種狀況。
循環時間長開銷大
。若是有不少個線程併發,CAS自旋可能會長時間不成功,會增大CPU的執行開銷。只能對一個變量進原子操做
。JDK1.5以後,新增AtomicReference類來處理這種狀況,能夠將多個變量放到一個對象中。