HashMap?面試?我是誰?我在哪?
來源:cnblogs.com/zhuoqingsen/p/HashMap.html
現在是晚上11點了,學校屠豬館的自習室因為太晚要關閉了。勤奮且疲憊的小魯班也從屠豬館出來了,正準備回宿舍洗洗睡,由於自習室位置比較偏僻所以是接收不到手機網路訊號的,因此小魯班從兜裡掏出手機的時候,資訊可真是炸了呀。小魯班心想,微信群平時都沒什麼人聊天,今晚肯定是發生了什麼大事。仔細一看,才發現原來是小魯班的室友達摩(光頭)拿到了阿里巴巴 Java 開發實習生的 Offer,此時小魯班真替他室友感到高興的同時,心裡也難免會產生一絲絲的失落感,那是因為自己投了很多份簡歷,別說拿不拿得到 Offer,就連給面試邀的公司也都寥寥無幾。小魯班這會可真是受到了一萬點真實暴擊。不過小魯班還是很樂觀的,很快調整了心態,帶上耳機,慢慢的走回了宿舍,正打算準備向他那神室友達摩取取經。
片刻後~
小魯班:666,聽說你拿到了阿里的 Offer,能透露一下面試內容和技巧嗎?
達摩:嘿嘿嘿,沒問題鴨,叫聲爸爸我就告訴你。
小魯班:耙耙(表面笑嘻嘻,心裡MMP)
達摩:其實我也不是很記得了(請繼續裝),但我還是記得那麼一些。如果你是面的 Java,首先當然是 Java 的基礎知識:資料結構(Map / List / Set等)、設計模式、演算法、執行緒相關、IO/NIO、序列化等等。其次是高階特性:反射機制、併發與鎖、JVM(GC策略 / 類載入機制 / 記憶體模型)等等。
小魯班:問這麼多內容,那豈不是一個人都面試很久嗎?
達摩:不是的,面試官一般都會用連環炮的方式提問的。
小魯班:你說的連環炮是什麼意思鴨?
達摩:那我舉個例子:
就比如問你 HashMap 是不是有序的?你回答不是有序的。
那面試官就會可能繼續問你,有沒有有序的Map實現類呢?你如果這個時候說不知道的話,那這塊問題就到此結束了。如果你說有 TreeMap 和 LinkedHashMap。
那麼面試官接下來就可能會問你,TreeMap 和 LinkedHashMap 是如何保證它的順序的?如果你回答不上來,那麼到此為止。如果你說 TreeMap 是通過實現 SortMap 介面,能夠把它儲存的鍵值對根據 key 排序,基於紅黑樹,從而保證 TreeMap 中所有鍵值對處於有序狀態。LinkedHashMap 則是通過插入排序(就是你 put 的時候的順序是什麼,取出來的時候就是什麼樣子)和訪問排序(改變排序把訪問過的放到底部)讓鍵值有序。
那麼面試官還會繼續問你,你覺得它們兩個哪個的有序實現比較好?如果你依然可以回答的話,那麼面試官會繼續問你,你覺得還有沒有比它更好或者更高效的實現方式?
無窮無盡深入,直到你回答不出來或者面試官認為問題到底了。
小魯班捏了一把汗,我去……這是魔鬼吧,那我們來試試唄(因為小魯班剛剛在自習室才看了這章的知識,想趁機裝一波逼,畢竟剛剛叫了聲爸爸~~)
於是達摩 and 小魯班就開始了對決:
1、為什麼用HashMap?
HashMap 是一個雜湊桶(陣列和連結串列),它儲存的內容是鍵值對 key-value 對映
HashMap 採用了陣列和連結串列的資料結構,能在查詢和修改方便繼承了陣列的線性查詢和連結串列的定址修改
HashMap 是非 synchronized,所以 HashMap 很快
HashMap 可以接受 null 鍵和值,而 Hashtable 則不能(原因就是 equlas() 方法需要物件,因為 HashMap 是後出的 API 經過處理才可以)
2、HashMap 的工作原理是什麼?
HashMap 是基於 hashing 的原理
我們使用 put(key, value) 儲存物件到 HashMap 中,使用 get(key) 從 HashMap 中獲取物件。當我們給 put() 方法傳遞鍵和值時,我們先對鍵呼叫 hashCode() 方法,計算並返回的 hashCode 是用於找到 Map 陣列的 bucket 位置來儲存 Node 物件。
這裡關鍵點在於指出,HashMap 是在 bucket 中儲存鍵物件和值物件,作為Map.Node 。
以下是 HashMap 初始化
簡化的模擬資料結構:
Node[] table = new Node[16]; // 雜湊桶初始化,table
class Node {
hash; //hash值
key; //鍵
value; //值
node next; //用於指向連結串列的下一層(產生衝突,用拉鍊法)
}
以下是具體的 put 過程(JDK1.8)
對 Key 求 Hash 值,然後再計算下標
如果沒有碰撞,直接放入桶中(碰撞的意思是計算得到的 Hash 值相同,需要放到同一個 bucket 中)
如果碰撞了,以連結串列的方式連結到後面
如果連結串列長度超過閥值(TREEIFY THRESHOLD==8),就把連結串列轉成紅黑樹,連結串列長度低於6,就把紅黑樹轉回連結串列
如果節點已經存在就替換舊值
如果桶滿了(容量16 * 載入因子0.75),就需要 resize(擴容2倍後重排)
以下是具體 get 過程
考慮特殊情況:如果兩個鍵的 hashcode 相同,你如何獲取值物件?
當我們呼叫 get() 方法,HashMap 會使用鍵物件的 hashcode 找到 bucket 位置,找到 bucket 位置之後,會呼叫 keys.equals() 方法去找到連結串列中正確的節點,最終找到要找的值物件。
3、有什麼方法可以減少碰撞?
擾動函式可以減少碰撞
原理是如果兩個不相等的物件返回不同的 hashcode 的話,那麼碰撞的機率就會小些。這就意味著存連結串列結構減小,這樣取值的話就不會頻繁呼叫 equal 方法,從而提高 HashMap 的效能(擾動即 Hash 方法內部的演算法實現,目的是讓不同物件返回不同 hashcode)。
使用不可變的、宣告作 final 物件,並且採用合適的 equals() 和 hashCode() 方法,將會減少碰撞的發生
不可變性使得能夠快取不同鍵的 hashcode,這將提高整個獲取物件的速度,使用 String、Integer 這樣的 wrapper 類作為鍵是非常好的選擇。
為什麼 String、Integer 這樣的 wrapper 類適合作為鍵?
因為 String 是 final,而且已經重寫了 equals() 和 hashCode() 方法了。不可變性是必要的,因為為了要計算 hashCode(),就要防止鍵值改變,如果鍵值在放入時和獲取時返回不同的 hashcode 的話,那麼就不能從 HashMap 中找到你想要的物件。
4、HashMap 中 hash 函式怎麼是實現的?
我們可以看到,在 hashmap 中要找到某個元素,需要根據 key 的 hash 值來求得對應陣列中的位置。如何計算這個位置就是 hash 演算法。
前面說過,hashmap 的資料結構是陣列和連結串列的結合,所以我們當然希望這個 hashmap 裡面的元素位置儘量的分佈均勻些,儘量使得每個位置上的元素數量只有一個。那麼當我們用 hash 演算法求得這個位置的時候,馬上就可以知道對應位置的元素就是我們要的,而不用再去遍歷連結串列。 所以,我們首先想到的就是把 hashcode 對陣列長度取模運算。這樣一來,元素的分佈相對來說是比較均勻的。
但是“模”運算的消耗還是比較大的,能不能找一種更快速、消耗更小的方式?我們來看看 JDK1.8 原始碼是怎麼做的(被樓主修飾了一下)
static final int hash(Object key) {
if (key == null){
return 0;
}
int h;
h = key.hashCode();返回雜湊值也就是hashcode
// ^ :按位異或
// >>>:無符號右移,忽略符號位,空位都以0補齊
//其中n是陣列的長度,即Map的陣列部分初始化長度
return (n-1)&(h ^ (h >>> 16));
}
簡單來說就是:
高16 bit 不變,低16 bit 和高16 bit 做了一個異或(得到的 hashcode 轉化為32位二進位制,前16位和後16位低16 bit 和高16 bit 做了一個異或)
(n·1) & hash = -> 得到下標
5、拉鍊法導致的連結串列過深,為什麼不用二叉查詢樹代替而選擇紅黑樹?為什麼不一直使用紅黑樹?
之所以選擇紅黑樹是為了解決二叉查詢樹的缺陷:二叉查詢樹在特殊情況下會變成一條線性結構(這就跟原來使用連結串列結構一樣了,造成層次很深的問題),遍歷查詢會非常慢。而紅黑樹在插入新資料後可能需要通過左旋、右旋、變色這些操作來保持平衡。引入紅黑樹就是為了查詢資料快,解決連結串列查詢深度的問題。我們知道紅黑樹屬於平衡二叉樹,為了保持“平衡”是需要付出代價的,但是該代價所損耗的資源要比遍歷線性連結串列要少。所以當長度大於8的時候,會使用紅黑樹;如果連結串列長度很短的話,根本不需要引入紅黑樹,引入反而會慢。
6、說說你對紅黑樹的見解?
每個節點非紅即黑
根節點總是黑色的
如果節點是紅色的,則它的子節點必須是黑色的(反之不一定)
每個葉子節點都是黑色的空節點(NIL節點)
從根節點到葉節點或空子節點的每條路徑,必須包含相同數目的黑色節點(即相同的黑色高度)
7、解決 hash 碰撞還有那些辦法?
開放定址法
當衝突發生時,使用某種探查技術在雜湊表中形成一個探查(測)序列。沿此序列逐個單元地查詢,直到找到給定的地址。按照形成探查序列的方法不同,可將開放定址法區分為線性探查法、二次探查法、雙重雜湊法等。
下面給一個線性探查法的例子:
問題:已知一組關鍵字為 (26,36,41,38,44,15,68,12,06,51),用除餘法構造雜湊函式,用線性探查法解決衝突構造這組關鍵字的雜湊表。
解答:
為了減少衝突,通常令裝填因子 α 由除餘法因子是13的雜湊函式計算出的上述關鍵字序列的雜湊地址為 (0,10,2,12,5,2,3,12,6,12)。
前5個關鍵字插入時,其相應的地址均為開放地址,故將它們直接插入 T[0]、T[10)、T[2]、T[12] 和 T[5] 中。
當插入第6個關鍵字15時,其雜湊地址2(即 h(15)=15%13=2)已被關鍵字 41(15和41互為同義詞)佔用。故探查 h1=(2+1)%13=3,此地址開放,所以將 15 放入 T[3] 中。
當插入第7個關鍵字68時,其雜湊地址3已被非同義詞15先佔用,故將其插入到T[4]中。
當插入第8個關鍵字12時,雜湊地址12已被同義詞38佔用,故探查 hl=(12+1)%13=0,而 T[0] 亦被26佔用,再探查 h2=(12+2)%13=1,此地址開放,可將12插入其中。
類似地,第9個關鍵字06直接插入 T[6] 中;而最後一個關鍵字51插人時,因探查的地址 12,0,1,…,6 均非空,故51插入 T[7] 中。
8、如果 HashMap 的大小超過了負載因子(load factor)定義的容量怎麼辦?
HashMap 預設的負載因子大小為0.75。也就是說,當一個 Map 填滿了75%的 bucket 時候,和其它集合類一樣(如 ArrayList 等),將會建立原來 HashMap 大小的兩倍的 bucket 陣列來重新調整 Map 大小,並將原來的物件放入新的 bucket 陣列中。這個過程叫作 rehashing。
因為它呼叫 hash 方法找到新的 bucket 位置。這個值只可能在兩個地方,一個是原下標的位置,另一種是在下標為 <原下標+原容量> 的位置。
9、重新調整 HashMap 大小存在什麼問題嗎?
重新調整 HashMap 大小的時候,確實存在條件競爭。
因為如果兩個執行緒都發現 HashMap 需要重新調整大小了,它們會同時試著調整大小。在調整大小的過程中,儲存在連結串列中的元素的次序會反過來。因為移動到新的 bucket 位置的時候,HashMap 並不會將元素放在連結串列的尾部,而是放在頭部。這是為了避免尾部遍歷(tail traversing)。如果條件競爭發生了,那麼就死迴圈了。多執行緒的環境下不使用 HashMap。
為什麼多執行緒會導致死迴圈,它是怎麼發生的?
HashMap 的容量是有限的。當經過多次元素插入,使得 HashMap 達到一定飽和度時,Key 對映位置發生衝突的機率會逐漸提高。這時候, HashMap 需要擴充套件它的長度,也就是進行Resize。
擴容:建立一個新的 Entry 空陣列,長度是原陣列的2倍
rehash:遍歷原 Entry 陣列,把所有的 Entry 重新 Hash 到新陣列
(這個過程比較燒腦,暫不作流程圖演示,有興趣去看看我的另一篇博文“HashMap擴容全過程”)
達摩:哎呦,小老弟不錯嘛~~意料之外呀
小魯班:嘿嘿,優秀吧,中場休息一波,我先喝口水
達摩:不僅僅是這些哦,面試官還會問你相關的集合類對比,比如:
10、HashTable
陣列 + 連結串列方式儲存
預設容量:11(質數為宜)
put操作:首先進行索引計算 (key.hashCode() & 0x7FFFFFFF)% table.length;若在連結串列中找到了,則替換舊值,若未找到則繼續;當總元素個數超過 容量 * 載入因子 時,擴容為原來 2 倍並重新雜湊;將新元素加到連結串列頭部
對修改 Hashtable 內部共享資料的方法新增了 synchronized,保證執行緒安全
11、HashMap 與 HashTable 區別
預設容量不同,擴容不同
執行緒安全性:HashTable 安全
效率不同:HashTable 要慢,因為加鎖
12、可以使用 CocurrentHashMap 來代替 Hashtable 嗎?
我們知道 Hashtable 是 synchronized 的,但是 ConcurrentHashMap 同步效能更好,因為它僅僅根據同步級別對 map 的一部分進行上鎖
ConcurrentHashMap 當然可以代替 HashTable,但是 HashTable 提供更強的執行緒安全性
它們都可以用於多執行緒的環境,但是當 Hashtable 的大小增加到一定的時候,效能會急劇下降,因為迭代時需要被鎖定很長的時間。由於 ConcurrentHashMap 引入了分割(segmentation),不論它變得多麼大,僅僅需要鎖定 Map 的某個部分,其它的執行緒不需要等到迭代完成才能訪問 Map。簡而言之,在迭代的過程中,ConcurrentHashMap 僅僅鎖定 Map 的某個部分,而 Hashtable 則會鎖定整個 Map
13、CocurrentHashMap(JDK 1.7)
CocurrentHashMap 是由 Segment 陣列和 HashEntry 陣列和連結串列組成
Segment 是基於重入鎖(ReentrantLock):一個資料段競爭鎖。每個 HashEntry 一個連結串列結構的元素,利用 Hash 演算法得到索引確定歸屬的資料段,也就是對應到在修改時需要競爭獲取的鎖。ConcurrentHashMap 支援 CurrencyLevel(Segment 陣列數量)的執行緒併發。每當一個執行緒佔用鎖訪問一個 Segment 時,不會影響到其他的 Segment
核心資料如 value,以及連結串列都是 volatile 修飾的,保證了獲取時的可見性
首先是通過 key 定位到 Segment,之後在對應的 Segment 中進行具體的 put 操作如下:
將當前 Segment 中的 table 通過 key 的 hashcode 定位到 HashEntry。
遍歷該 HashEntry,如果不為空則判斷傳入的 key 和當前遍歷的 key 是否相等,相等則覆蓋舊的 value
不為空則需要新建一個 HashEntry 並加入到 Segment 中,同時會先判斷是否需要擴容
最後會解除在 1 中所獲取當前 Segment 的鎖
雖然 HashEntry 中的 value 是用 volatile 關鍵詞修飾的,但是並不能保證併發的原子性,所以 put 操作時仍然需要加鎖處理
首先第一步的時候會嘗試獲取鎖,如果獲取失敗肯定就有其他執行緒存在競爭,則利用 scanAndLockForPut() 自旋獲取鎖
嘗試自旋獲取鎖
如果重試的次數達到了 MAX_SCAN_RETRIES 則改為阻塞鎖獲取,保證能獲取成功。最後解除當前 Segment 的鎖
14、CocurrentHashMap(JDK 1.8)
CocurrentHashMap 拋棄了原有的 Segment 分段鎖,採用了 CAS + synchronized 來保證併發安全性。其中的 val next 都用了 volatile 修飾,保證了可見性。
最大特點是引入了 CAS
藉助 Unsafe 來實現 native code。CAS有3個運算元,記憶體值 V、舊的預期值 A、要修改的新值 B。當且僅當預期值 A 和記憶體值 V 相同時,將記憶體值V修改為 B,否則什麼都不做。Unsafe 藉助 CPU 指令 cmpxchg 來實現。
CAS 使用例項
對 sizeCtl 的控制都是用 CAS 來實現的:
-1 代表 table 正在初始化
N 表示有 -N-1 個執行緒正在進行擴容操作
如果 table 未初始化,表示table需要初始化的大小
如果 table 初始化完成,表示table的容量,預設是table大小的0.75倍,用這個公式算 0.75(n – (n >>> 2))
CAS 會出現的問題:ABA
解決:對變數增加一個版本號,每次修改,版本號加 1,比較的時候比較版本號。
put 過程
根據 key 計算出 hashcode
判斷是否需要進行初始化
通過 key 定位出的 Node,如果為空表示當前位置可以寫入資料,利用 CAS 嘗試寫入,失敗則自旋保證成功
如果當前位置的 hashcode == MOVED == -1,則需要進行擴容
如果都不滿足,則利用 synchronized 鎖寫入資料
如果數量大於 TREEIFY_THRESHOLD 則要轉換為紅黑樹
get 過程
根據計算出來的 hashcode 定址,如果就在桶上那麼直接返回值
如果是紅黑樹那就按照樹的方式獲取值
就不滿足那就按照連結串列的方式遍歷獲取值
此時躺著床上的張飛哄了一聲:睡覺了睡覺了~
見此不太妙:小魯班立馬回到床上把被子蓋過頭,心裡有一絲絲愉悅感。不對,好像還沒洗澡……
by the way
ConcurrentHashMap 在 Java 8 中存在一個 bug 會進入死迴圈,原因是遞迴建立 ConcurrentHashMap 物件,但是在 JDK 1.9 已經修復了。場景重現如下:
public class ConcurrentHashMapDemo{
private Map<Integer,Integer> cache =new ConcurrentHashMap<>(15);
public static void main(String[]args){
ConcurrentHashMapDemo ch = new ConcurrentHashMapDemo();
System.out.println(ch.fibonaacci(80));
}
public int fibonaacci(Integer i){
if(i==0||i ==1) {
return i;
}
return cache.computeIfAbsent(i,(key) -> {
System.out.println("fibonaacci : "+key);
return fibonaacci(key -1)+fibonaacci(key - 2);
});
}
}
(完)
Java團長
專注於Java乾貨分享
掃描上方二維碼獲取更多Java乾貨
相關文章
- 面試官問我HTTP,我真的是面試HTTP
- 阿里面試官用HashMap把我問倒了阿里面試HashMap
- 面試官問我Redis叢集,我真的是面試Redis
- 面試官問我MySQL調優,我真的是面試MySql
- 故障分析 | 是誰偷走了我的 IO
- 再也不怕面試官問我JDK8 HashMap了面試JDKHashMap
- 我為誰工作
- 誰說我們程式設計師悶騷,我跟誰急程式設計師
- 面試官問我JVM記憶體結構,我真的是面試JVM記憶體
- 如果我是Android面試官二Android面試
- 如果我是Android面試官一Android面試
- 面試官問我:什麼是JavaScript閉包,我該如何回答面試JavaScript
- java面試官:程式設計師,請你告訴我是誰把公司面試題洩露給你的?Java程式設計師面試題
- 幽默:架構師在哪裡?是誰?架構
- 誰動了我的 Redis ?Redis
- 誰動了我的MySQL?MySql
- Cate:我是如何準備Google面試的Go面試
- 我是如何準備技術面試的面試
- 突擊面試,我還是太菜了。面試
- 面試官問我MySQL索引,我面試MySql索引
- 我是設計師面試官,你有什麼想問我的?面試
- 我的面試面試
- 誰再黑程式設計師我就打誰程式設計師
- 誰能幫我做一下這個簡單的專案啊!我是初學者。
- 如果我是推薦演算法面試官,我會問哪些問題?演算法面試
- 誰動了我的程式碼!?
- 我曾經是怎麼做面試官的面試
- 我是如何用 ThreadLocal 虐面試官的?thread面試
- 《我是面試官》設計模式-單例模式面試設計模式單例
- 我裂開,面試中的HashMap 奪命二十一問!你看看這是人該乾的事嗎?面試HashMap
- 我是如何評估面試者的軟技能的?面試
- 假如我是面試官,我會這樣虐你面試
- 『假如我是面試官』RabbitMQ我會這樣問面試MQ
- 誰鎖住了我的資料表
- 誰動了我的指標? (轉)指標
- 我是MJ
- 面試官問我TCP三次握手和四次揮手,我真的是面試TCP
- 我們應該定位在哪裡?