每日一面系列之HashMap奪命連環問

2021-10-07 14:49:56 字數 4610 閱讀 7791

底層資料結構是雜湊表結構(鍊錶雜湊:陣列+單向鍊錶),結合了陣列和鍊錶的優點,當鍊表長度超過8時,鍊錶會轉為紅黑樹。陣列中的每乙個元素都是鍊錶。總結來說就是hashmap在jdk1.8之前底層是由陣列+鍊錶實現的,在jdk1.8開始底層是由陣列+鍊錶或者陣列+紅黑樹實現的。

追問:為什麼在1.8中增加紅黑樹?

當需要查詢某個元素的時候,線性探索是最直白的方式,它會把所有資料遍歷一遍直到找到你所查詢的資料,對於陣列和鍊錶這種線性結構來說,當鍊表長度過長(資料有成百上千)的時候,會造成鍊錶過深的問題,這種查詢方式效率極低,時間複雜度是o(n)。簡單來說紅黑樹的出現就是為了提高資料檢索的速度。

追問:鍊錶過深問題為什麼不用二叉查詢樹代替,而選擇紅黑樹?為什麼不一直使用紅黑樹?

二叉樹在特殊情況下會變成一條線性結構,這就跟原來的鍊錶結構一樣了,選擇紅黑樹就是為了解決二叉樹的缺陷。

紅黑樹在插入資料的時候需要通過左旋、右旋、變色這些操作來保持平衡,為了保持這種平衡是需要付出代價的。當鍊表很短的時候,沒必要使用紅黑樹,否則會導致效率更低,當鍊表很長的時候,使用紅黑樹,保持平衡的操作所消耗的資源要遠小於遍歷鍊錶鎖消耗的效率,所以才會設定乙個閾值,去判斷什麼時候使用鍊錶,什麼時候使用紅黑樹。

追問:講一下你對紅黑樹的認識

儲存物件時,將key和vaule傳給put()方法:

判斷陣列是否為空,為空進行初始化;

不為空,計算 k 的 hash 值,通過(n - 1) & hash計算應當存放在陣列中的下標 index;

檢視 table[index] 是否存在資料,沒有資料就構造乙個node節點存放在 table[index] 中;

存在資料,說明發生了hash衝突(存在二個節點key的hash值一樣), 繼續判斷key是否相等,相等,用新的value替換原資料(onlyifabsent為false);

如果不相等,判斷當前節點型別是不是樹型節點,如果是樹型節點,創造樹型節點插入紅黑樹中;(如果當前節點是樹型節點證明當前已經是紅黑樹了)

如果不是樹型節點,建立普通node加入鍊錶中;判斷鍊錶長度是否大於8並且陣列長度大於64,大於的話鍊錶轉換為紅黑樹;

插入完成之後判斷當前節點數是否大於閾值(capacity*loadfactor),如果大於開始擴容為原陣列的二倍。

下面以流程圖方式更加直觀的看一下插入流程:

獲取物件時,將key傳給get()方法:

呼叫hash(key)方法獲取key對應的hash值從而獲取該鍵值對在陣列中的下標。

對鍊錶進行順序遍歷,使用equals()方法查詢鍊錶中相等的key對應的value值。

追問:說一下陣列是怎麼擴容的?

建立乙個新陣列,新陣列初始化容量大小是舊陣列的兩倍,對原陣列中元素重新進行一次hash從而定位在新陣列中的儲存位置,元素在新陣列中的位置只有兩種,原下標位置或原下標+舊陣列的大小。

追問:為什麼要對原陣列中元素再重新進行一次hash?直接複製到新陣列不行嗎?

因為陣列長度擴大以後hash規則也會隨之變化。

hash的公式—> index = hashcode(key) & (length - 1)

追問:在插入元素的時候,jdk1.7與jdk1.8有什麼不同?

1.7是先判斷是否需要擴容,再進行插入操作。1.8是先插入,插入完成之後再判斷是否需要擴容。

注:hashcode是用來定位的,定鍵值對在陣列中的儲存位置。equals()方法是用來定性的,比較兩個物件是否相等。

新值會作為鍊錶的頭部替換原來的值,原來的值會被順推到鍊錶當中。下面以**方式說明一下:

設計者認為後來插入的值被查詢的概率比較高,使用頭插法可以提高查詢的效率。

jdk1.8之前擴容的時候,頭插法會導致鍊錶反轉,在多執行緒情況下會出現環形鍊錶,導致取值的時候出現死迴圈,jdk1.8開始在同樣的前提下就不會導致死迴圈,因為在擴容轉移前後鍊錶的順序不變,保持之前節點的引用關係。

例:a執行緒和b執行緒同時向同乙個下標位置插入節點,遇到容量不夠開始擴容,重新hash,放置元素,採用頭插法,後遍歷到的b節點放入了頭部,這樣形成了環,如下圖所示:

使用new hashmap()不傳值,預設大小是16,負載因子是0.75。如果傳入引數k,那麼初始化容量大小為大於k的2的最小整數冪。比如傳入的是10,那麼初始化容量大小就是16(2的4次方)。

注:有興趣可以思考一下為什麼負載因子是0.75,而不是0.6,也不是0.5。可以參考為什麼 hashmap 的載入因子是0.75。

追問:為什麼hashmap的陣列長度要取2的整數冪?

因為這樣陣列長度-1正好相當於乙個「低位掩碼」。「與」操作的結果就是雜湊值的高位全部歸零,只保留低位值,用來做陣列下標訪問。以初始長度16為例,16-1=15。2進製表示是00000000 00000000 00001111。和某雜湊值做「與」操作如下,結果就是擷取了最低的四位值。

key的hashcode是乙個32位的int型別值,hash函式就是將hashcode的高16位和低16位進行異或運算。

追問:雜湊函式為什麼這麼設計?

這是乙個擾動函式,這樣設計的原因主要有兩點:

可以最大程度的降低hash碰撞的概率(hash值越分散越好);

因為是高頻操作,所以採用位運算,讓演算法更加高效;

不是,在多執行緒的情況下,1.7的hashmap會導致死迴圈、資料丟失、資料覆蓋。在1.8中如果有多個執行緒同時put()元素還是會存在資料覆蓋的問題。以1.8位例,a執行緒判斷index位置為空後正好掛起,b執行緒開始向index位置寫入節點資料,這時a執行緒恢復現場,執行賦值操作,就把a執行緒的資料給覆蓋了。

追問:如何解決這個執行緒不安全的問題?

可以使用hashtable、collections.synchronizedmap、以及concurrenthashmap這些執行緒安全的map。

追問:分別講一下這幾種map都是如何實現執行緒安全的?

hashtable是直接在操作方法上加synchronized關鍵字,鎖住整個陣列,粒度比較大;

collections.synchronizedmap是使用collections集合工具的內部類,通過傳入map封裝出乙個synchronizedmap物件,內部定義了乙個物件鎖,方法內通過物件鎖實現;

concurrenthashmap在jdk1.7中使用分段鎖,降低了鎖粒度,讓併發度大大提高,在jdk 1.8 中直接採用了cas(無鎖演算法)+ synchronized的方式來實現執行緒安全。

底層資料結構:1.7中是陣列+鍊錶。1.8中是陣列+鍊錶或陣列+紅黑樹;

元素插入方式:1.7是頭插法插入鍊錶。1.7是尾插法插入鍊錶;

節點型別:1.7中陣列中節點型別是entry節點,1.8中陣列中節點型別是node節點;

元素插入流程:1.7中是先判斷是否需要擴容,再插入。1.8中是先插入,插入成功之後再判斷是否需要擴容;

擴容方式:1.7中需要對原陣列中元素重新進行hash定位在新陣列中的位置。1.8中採用更簡單的邏輯判斷,原下標位置或原下標+舊陣列的大小。

是無序的,根據hash值隨機插入。

追問:你知道哪些有序的map?

linkedhashmap和treemap。

追問:說一下這兩種map分別是怎麼實現有序的

linkedhashmap: linkedhashmap內部維護了乙個單鏈表,有頭尾節點,同時linkedhashmap節點entry內部除了繼承hashmap的node屬性,還有before 和 after用於標識前置節點和後置節點。可以實現按插入的順序或訪問順序排序。

treehashmap:treemap是按照key的自然順序或者comprator的順序進行排序,內部是通過紅黑樹來實現。所以要麼key所屬的類實現comparable介面,或者自定義乙個實現了comparator介面的比較器,傳給treemap用於key的比較。

linkedhashmap 儲存了記錄的插入順序,在用 iterator 遍歷時,先取到的記錄肯定是先插入的;遍歷比 hashmap 慢。treemap 實現 sortmap 介面,能夠把它儲存的記錄根據鍵排序(預設按鍵值公升序排序,也可以指定排序的比較器)

追問:講一下這三種map的使用場景

一般情況下,使用最多的是 hashmap。

hashmap:在 map 中插入、刪除和定位元素時;

treemap:在需要按自然順序或自定義順序遍歷鍵的情況下;

linkedhashmap:在需要輸出的順序和輸入的順序相同的情況下。

每日一面 限制使用者裝置

使用者登入,儲存30天的免登,只允許兩個裝置登入,如果有第三個裝置登入,踢掉第乙個。改密碼的時候,所有裝置需要下線。這個邏輯怎麼實現呢?使用 redis 儲存使用者 登入的裝置實現,利用 zset。儲存結構如下 每個使用者乙個 zset 假設就是以使用者 id 作為 zset 的 key 裡面的 k...

每日一面 關於通訊協議

關於http協議參考博文 http協議詳解 那麼get和post與資料如何傳遞到底有沒有關係?get和post是由http協議定義的。在http協議中,method和data url,body,header 是正交的兩個概念,也就是說,使用哪個method與應用層的資料如何傳輸是沒有相互關係的。ht...

每日一面 關於推理題

乙個5公升的桶和乙個3公升的桶,如何得到一桶4公升的水?數字移位 題目是這樣的 乙個n位數,個位數是6,將6移動到最前面 首位 然後形成乙個新的n位數,新的n位數為舊的n位數的4倍,問最小的n位數是多少?先給一種逆推法 個位數是6,新的n位數為舊的4倍,那麼舊的n位數肯定是 46 x未知,x有幾位待...