數據結構(HashMap和ConcurrentHashMap )

應爲面試中數據結構方面問的最多的就是hashMap,所以今天對hashMap做了一個總結。
給定的默認容量爲 16,負載因子爲 0.75。Map 在使用過程中不斷的往裏面存放數據,當數量達到了 16 * 0.75 = 12 就需要將當前 16 的容量進行擴容,而擴容這個過程涉及到 rehash、複製數據等操作,所以非常消耗性能。

因此通常建議能提前預估 HashMap 的大小最好,儘量的減少擴容帶來的性能損耗。
在這裏插入圖片描述
Entry 是 HashMap 中的一個內部類,從他的成員變量很容易看出:
key 就是寫入時的鍵。
value 自然就是值。
開始的時候就提到 HashMap 是由數組和鏈表組成,所以這個 next 就是用於實現鏈表結構。
hash 存放的是當前 key 的 hashcode。

HashMap實際上是一個“鏈表散列”的數據結構,即數組和鏈表的結合體
數組:存儲區間連續,佔用內存嚴重,尋址容易,插入刪除困難;
鏈表:存儲區間離散,佔用內存比較寬鬆,尋址困難,插入刪除容易;
Hashmap綜合應用了這兩種數據結構,實現了尋址容易,插入刪除也容易。
hashMap結構
在這裏插入圖片描述
1.7實現
PUT方法:
判斷當前數組是否需要初始化。
如果 key 爲空,則 put 一個空值進去。
根據 key 計算出 hashcode。
根據計算出的 hashcode 定位出所在桶。
如果桶是一個鏈表則需要遍歷判斷裏面的 hashcode、key 是否和傳入 key 相等,如果相等則進行覆蓋,並返回原來的值。
如果桶是空的,說明當前位置沒有數據存入;新增一個 Entry 對象寫入當前位置
GET方法:
首先也是根據 key 計算出 hashcode,然後定位到具體的桶中。
判斷該位置是否爲鏈表。
不是鏈表就根據 key、key 的 hashcode 是否相等來返回值。
爲鏈表則需要遍歷直到 key 及 hashcode 相等時候就返回值。
啥都沒取到就直接返回 null 。

1.7缺點:
當 Hash 衝突嚴重時,在桶上形成的鏈表會變的越來越長,這樣在查詢時的效率就會越來越低;時間複雜度爲 O(N)。

在這裏插入圖片描述
1.8 修改之前在發生嚴重hash碰撞時候,在鏈表達到一定的閾值,會把鏈表轉化爲紅黑樹進行存儲,修改爲紅黑樹之後查詢效率直接提高到了 O(logn)
PUT方法:
1.判斷當前桶是否爲空,空的就需要初始化(resize 中會判斷是否進行初始化)。
2.根據當前 key 的 hashcode 定位到具體的桶中並判斷是否爲空,爲空表明沒有 Hash 衝突就直接在當前位置創建一個新桶即可。
3.如果當前桶有值( Hash 衝突),那麼就要比較當前桶中的 key、key 的 hashcode 與寫入的 key 是否相等,相等就賦值給 e,在第 8 步的時候會統一進行賦值及返回。
4.如果當前桶爲紅黑樹,那就要按照紅黑樹的方式寫入數據。
5.如果是個鏈表,就需要將當前的 key、value 封裝成一個新節點寫入到當前桶的後面(形成鏈表)。
6.接着判斷當前鏈表的大小是否大於預設的閾值,大於時就要轉換爲紅黑樹。
7.如果在遍歷過程中找到 key 相同時直接退出遍歷。
8.如果 e != null 就相當於存在相同的 key,那就需要將值覆蓋。
9.最後判斷是否需要進行擴容。
GET方法:
首先將 key hash 之後取得所定位的桶。
如果桶爲空則直接返回 null 。
否則判斷桶的第一個位置(有可能是鏈表、紅黑樹)的 key 是否爲查詢的 key,是就直接返回 value。
如果第一個不匹配,則判斷它的下一個是紅黑樹還是鏈表。
紅黑樹就按照樹的查找方式返回值。
不然就按照鏈表的方式遍歷匹配返回值。
但是這兩種方式還是會有問題,那就是在併發場景下使用時容易出現死循環。
原因:HashMap 擴容的時候會調用 resize() 方法,就是這裏的併發操作容易在一個桶上形成環形鏈表;這樣當獲取一個不存在的 key 時,計算出的 index 正好是環形鏈表的下標就會出現死循環。

ConcurrentHashMap
1.7版本
在這裏插入圖片描述
Segment 數組、HashEntry 組成,和 HashMap 一樣,仍然是數組加鏈表。
它的核心成員變量
在這裏插入圖片描述
和 HashMap 非常類似,唯一的區別就是其中的核心數據如 value ,以及鏈表都是 volatile 修飾的,保證了獲取時的可見性。
原理上來說:ConcurrentHashMap 採用了分段鎖技術,其中 Segment 繼承於 ReentrantLock。不會像 HashTable 那樣不管是 put 還是 get 操作都需要做同步處理,理論上 ConcurrentHashMap 支持 CurrencyLevel (Segment 數組數量)的線程併發。每當一個線程佔用鎖訪問一個 Segment 時,不會影響到其他的 Segment。

雖然 HashEntry 中的 value 是用 volatile 關鍵詞修飾的,但是並不能保證併發的原子性,所以 put 操作時仍然需要加鎖處理。

首先第一步的時候會嘗試獲取鎖,如果獲取失敗肯定就有其他線程存在競爭,則利用 scanAndLockForPut() 自旋獲取鎖。

1.嘗試自旋獲取鎖。
2.如果重試的次數達到了 MAX_SCAN_RETRIES 則改爲阻塞鎖獲取,保證能獲取成功

PUT方法
1.將當前 Segment 中的 table 通過 key 的 hashcode 定位到 HashEntry。
2.遍歷該 HashEntry,如果不爲空則判斷傳入的 key 和當前遍歷的 key 是否相等,相等則覆蓋舊的 value。
3.不爲空則需要新建一個 HashEntry 並加入到 Segment 中,同時會先判斷是否需要擴容。
4.最後會解除在 1 中所獲取當前 Segment 的鎖。
GET方法:
只需要將 Key 通過 Hash 之後定位到具體的 Segment ,再通過一次 Hash 定位到具體的元素上。
由於 HashEntry 中的 value 屬性是用 volatile 關鍵詞修飾的,保證了內存可見性,所以每次獲取時都是最新值。
ConcurrentHashMap 的 get 方法是非常高效的,因爲整個過程都不需要加鎖。
缺點:雖然解決了併發,但是查詢遍歷鏈表效率太低。

1.8版本:CAS + synchronized 來保證併發安全性
在這裏插入圖片描述
val next 都用了 volatile 修飾,保證了可見性。
PUT方法:
1.根據 key 計算出 hashcode 。
2.判斷是否需要進行初始化。
3.f 即爲當前 key 定位出的 Node,如果爲空表示當前位置可以寫入數據,利用 CAS 嘗試寫入,失敗則自旋保證成功。
4.如果當前位置的 hashcode == MOVED == -1,則需要進行擴容。
5.如果都不滿足,則利用 synchronized 鎖寫入數據。
6.如果數量大於 TREEIFY_THRESHOLD 則要轉換爲紅黑樹。
GET方法:
根據計算出來的 hashcode 尋址,如果就在桶上那麼直接返回值。
如果是紅黑樹那就按照樹的方式獲取值。
就不滿足那就按照鏈表的方式遍歷獲取值。

其實這塊也是面試的重點內容,通常的套路是:
談談你理解的 HashMap,講講其中的 get put 過程。
1.8 做了什麼優化?
是線程安全的嘛?
不安全會導致哪些問題?
如何解決?有沒有線程安全的併發容器?
ConcurrentHashMap 是如何實現的? 1.7、1.8 實現有何不同?爲什麼這麼做?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章