緣由在於HashMap在多線程狀況下,執行resize()進行擴容時容易形成死循環。
擴容思路爲它要建立一個大小爲原來兩倍的數組,保證新的容量仍爲2的N次方,從而保證上述尋址方式仍然適用。擴容後將原來的數組重新插入到新的數組中。這個過程稱爲reHash。java
【單線程下的reHash】 數組
單線程reHash徹底沒有問題。安全
【多線程下的reHash】 數據結構
咱們假設有兩個線程同時須要執行resize操做,咱們原來的桶數量爲2,記錄數爲3,須要resize桶到4,原來的記錄分別爲:[3,A],[7,B],[5,C],在原來的map裏面,咱們發現這三個entry都落到了第二個桶裏面。
假設線程thread1執行到了transfer方法的Entry next = e.next這一句,而後時間片用完了,此時的e = [3,A], next = [7,B]。線程thread2被調度執行而且順利完成了resize操做,須要注意的是,此時的[7,B]的next爲[3,A]。此時線程thread1從新被調度運行,此時的thread1持有的引用是已經被thread2 resize以後的結果。線程thread1首先將[3,A]遷移到新的數組上,而後再處理[7,B],而[7,B]被連接到了[3,A]的後面,處理完[7,B]以後,就須要處理[7,B]的next了啊,而經過thread2的resize以後,[7,B]的next變爲了[3,A],此時,[3,A]和[7,B]造成了環形鏈表,在get的時候,若是get的key的桶索引和[3,A]和[7,B]同樣,那麼就會陷入死循環。多線程
Fail-safe和iterator迭代器相關。若是某個集合對象建立了Iterator或者ListIterator,而後其它的線程試圖「結構上」更改集合對象,將會拋出ConcurrentModificationException異常。但其它線程能夠經過set()方法更改集合對象是容許的,由於這並無從「結構上」更改集合。可是假如已經從結構上進行了更改,再調用set()方法,將會拋出IllegalArgumentException異常。結構上的更改指的是刪除或者插入一個元素,這樣會影響到map的結構。併發
解決辦法:可使用Collections的synchronizedMap方法構造一個同步的map,或者直接使用線程安全的ConcurrentHashMap來保證不會出現fail-fast策略。不支持併發操做ssh
Java中的數據存儲方式有兩種結構,一種是數組,另外一種就是鏈表,前者的特色是連續空間,尋址迅速,可是在增刪元素的時候會有較大幅度的移動,因此數組的特色是查詢速度快,增刪較慢。高併發
而鏈表因爲空間不連續,尋址困難,增刪元素只需修改指針,因此鏈表的特色是查詢速度慢、增刪快。性能
那麼有沒有一種數據結構來綜合一下數組和鏈表以便發揮他們各自的優點?答案就是哈希表。優化
HashMap 裏面是一個數組,而後數組中每一個元素是一個單向鏈表。
上圖中,每一個綠色的實體是嵌套類 Entry 的實例,Entry 包含四個屬性:key, value, hash 值和用於單向鏈表的 next。
capacity:當前數組容量,始終保持 2^n,能夠擴容,擴容後數組大小爲當前的 2 倍。
loadFactor:負載因子,默認爲 0.75。
threshold:擴容的閾值,等於 capacity * loadFactor
在第一個元素插入 HashMap 的時候作一次數組的初始化,就是先肯定初始的數組大小,並計算數組擴容的閾值。
1. 求 key 的 hash 值
int
hash = hash(key);
2. 找到對應的數組下標
int
i = indexFor(hash, table.length);
3.放入鏈表頭部
在插入新值的時候,若是當前的 size 已經達到了閾值,而且要插入的數組位置上已經有元素,那麼就會觸發擴容,擴容後,數組大小爲原來的 2 倍。
1.根據 key 計算 hash 值。
2.找到相應的數組下標:hash & (length – 1)。
3.遍歷該數組位置處的鏈表,直到找到相等(==或equals)的 key。
——基於分段鎖的ConcurrentHashMap
Segment繼承自J.U.C裏的ReetrantLock,因此能夠很方便的對Segment進行上鎖。即分段鎖。理論上最大併發數是和segment的個數是想等的。
initialCapacity:初始容量,這個值指的是整個 ConcurrentHashMap 的初始容量,實際操做的時候須要平均分給每一個 Segment。
loadFactor:負載因子,以前咱們說了,Segment 數組不能夠擴容,因此這個負載因子是給每一個 Segment 內部使用的。擴容是 segment 數組某個位置內部的數組 HashEntry<k,v>[] 進行擴容,擴容後,容量爲原來的 2 倍。
1. 計算 key 的 hash 值
2. 根據 hash 值找到 Segment 數組中的位置
3.再利用 hash 值,求應該放置的segment 內部的數組下標
4.添加到頭部
1.計算 hash 值,找到 segment 數組中的具體位置,或咱們前面用的「槽」
2.槽中也是一個數組,根據 hash 找到數組中具體的位置
3.到這裏是鏈表了,順着鏈表進行查找便可
Java8 對 HashMap 進行了一些修改,最大的不一樣就是利用了紅黑樹,因此其由 數組+鏈表+紅黑樹 組成。
根據 Java7 HashMap 的介紹,咱們知道,查找的時候,根據 hash 值咱們可以快速定位到數組的具體下標,可是以後的話,須要順着鏈表一個個比較下去才能找到咱們須要的,時間複雜度取決於鏈表的長度,爲 O(n)。
Java 8爲進一步提升併發性,摒棄了分段鎖的方案,而是直接使用一個大的數組。同時爲了提升哈希碰撞下的尋址性能,Java 8在鏈表長度超過必定閾值(8)時將鏈表(尋址時間複雜度爲O(N))轉換爲紅黑樹(尋址時間複雜度爲O(long(N)))。
java8也是經過計算key的hash值和數組長度值進行取模肯定該key在數組中的索引。可是java8引入紅黑樹,即便hash衝突比較高,尋址效率也會是比較高的。
來一張圖簡單示意一下吧:
Java7 中使用 Entry 來表明每一個 HashMap 中的數據節點,Java8 中使用 Node,基本沒有區別,都是 key,value,hash 和 next 這四個屬性,不過,Node 只能用於鏈表的狀況,紅黑樹的狀況須要使用 TreeNode。
咱們根據數組元素中,第一個節點數據類型是 Node 仍是 TreeNode 來判斷該位置下是鏈表仍是紅黑樹的。
參考get過程
——基於CAS的ConcurrentHashMap
比java8的HashMap複雜不少,可是結構差很少全。
對於put操做,若是Key對應的數組元素爲null,則經過CAS操做將其設置爲當前值。若是Key對應的數組元素(也即鏈表表頭或者樹的根元素)不爲null,則對該元素使用synchronized關鍵字申請鎖,而後進行操做。若是該put操做使得當前鏈表長度超過必定閾值,則將該鏈表轉換爲樹,從而提升尋址效率。
對於讀操做,因爲數組被volatile關鍵字修飾,所以不用擔憂數組的可見性問題。同時每一個元素是一個Node實例(Java 7中每一個元素是一個HashEntry),它的Key值和hash值都由final修飾,不可變動,無須關心它們被修改後的可見性問題。而其Value及對下一個元素的引用由volatile修飾,可見性也有保障
HashMap和ConcurrentHashMap對比:
HashMap和HashTable的對比:
(1)HashMap是非線程安全的,HashTable是線程安全的。
(2)HashMap的鍵和值都容許有null存在,而HashTable則都不行。
(3)由於線程安全、哈希效率的問題,HashMap效率比HashTable的要高。
HashTable和ConcurrentHashMap對比:
HashTable裏使用的是synchronized關鍵字,這實際上是對對象加鎖,鎖住的都是對象總體,當Hashtable的大小增長到必定的時候,性能會急劇降低,由於迭代時須要被鎖定很長的時間。ConcurrentHashMap相對於HashTable的syn關鍵字鎖的粒度更精細了一些,併發性能更好。
問題:在put的時候是放在鏈表頭部仍是尾部?
jdk1.7以前是放在鏈表頭部在jdk1.8以後是放在尾部。