關注「Java後端技術全棧」回覆「000」獲取大量電子書java
在面試中,HashMap基本必問,只是問法各有不一樣而已。曾經我也和不少面試官聊過關於HashMap的話題,使用HashMap就能考察面試者的不少知識點。不幸的是,很大部分人都拜倒在HashMap的石榴裙底下。面試
HashMap爲何如此受面試官青睞?算法
我以爲其中有4個緣由:編程
HashMap在咱們工做中使用頻率至關高。後端
Java基礎(能夠經過此Java集合)數組
線程安全問題(能夠經過這個問題引入多線程併發編程的相關問題)緩存
大廠都在問,豈能不問?(不問的話,顯得面試官沒有水平)安全
下面就是我給你們準備的HashMap連環炮,這個連環炮就至關於高考真題演練同樣,可能沒有徹底同樣的,只是問法不一樣罷了,這個主要得益於我們漢語博大精深。微信
下面是HashMap的25連環炮:markdown
1:說說HashMap 底層數據結構是怎樣的?
2:談一下HashMap的特性?
3:使用HashMap時,當兩個對象的 hashCode 相同怎麼辦?
4:HashMap 的哈希函數怎麼設計的嗎?
5:HashMap遍歷方法有幾種?
6:爲何採用 hashcode 的高 16 位和低 16 位異或能下降 hash 碰撞?hash 函數能不能直接用 key 的 hashcode?
7:解決hash衝突的有幾種方法?
8:爲何要用異或運算符?
9.:HashMap 的 table 的容量如何肯定?
10:請解釋一下HashMap的參數loadFactor,它的做用是什麼
11:說說HashMap中put方法的過程
12:當鏈表長度 >= 8時,爲何要將鏈表轉換成紅黑樹?
13:new HashMap(18);此時HashMap初始容量爲多少?
14:說說resize擴容的過程
15:說說hashMap中get是如何實現的?
16:拉鍊法致使的鏈表過深問題爲何不用二叉查找樹代替,而選擇紅黑樹?爲何不一直使用紅黑樹?
17:說說你對紅黑樹的瞭解
18:JDK8中對HashMap作了哪些改變?
19:HashMap 中的 key 咱們可使用任何類做爲 key 嗎?
20:HashMap 的長度爲何是 2 的 N 次方呢?
21:HashMap,LinkedHashMap,TreeMap 有什麼區別?
22:說說什麼是 fail-fast?
23:HashMap 和 HashTable 有什麼區別?
24:HashMap 是線程安全的嗎?
25:如何規避 HashMap 的線程不安全?
26:HashMap 和 ConcurrentHashMap 的區別?
27:爲何 ConcurrentHashMap 比 HashTable 效率要高?
28:說說 ConcurrentHashMap中 鎖機制
29:在 JDK 1.8 中,ConcurrentHashMap 爲何要使用內置鎖 synchronized 來代替重入鎖 ReentrantLock?
30:能對ConcurrentHashMap 作個簡單介紹嗎?
31:熟悉ConcurrentHashMap 的併發度嗎?
....
(須要思惟導圖的,請加我微信tj20120622,免費贈予)
下面咱們正式開始連環炮
HashMap 底層是 hash 數組和單向鏈表實現,jdk8後採用數組+鏈表+紅黑樹的數據結構。
若是第一題沒問,直接問原理,那就必須把HashMap的數據結構說清楚。
HashMap 底層是 hash 數組和單向鏈表實現,JDK8後採用數組+鏈表+紅黑樹的數據結構。
咱們經過put和get存儲和獲取對象。當咱們給put()方法傳遞鍵和值時,先對鍵作一個hashCode()的計算來獲得它在bucket數組中的位置來存儲Entry對象。當獲取對象時,經過get獲取到bucket的位置,再經過鍵對象的equals()方法找到正確的鍵值對,而後在返回值對象。
由於HashCode 相同,不必定就是相等的(equals方法比較),因此兩個對象所在數組的下標相同,"碰撞"就此發生。又由於 HashMap 使用鏈表存儲對象,這個 Node 會存儲到鏈表中。
hash 函數是先拿到經過 key 的 hashCode ,是 32 位的 int 值,而後讓 hashCode 的高 16 位和低 16 位進行異或操做。兩個好處:
必定要儘量下降 hash 碰撞,越分散越好;
算法必定要儘量高效,由於這是高頻操做, 所以採用位運算;
Iterator 迭代器
最多見的使用方式,可同時獲得 key、value 值
使用 foreach 方式(JDK1.8 纔有)
經過 key 的 set 集合遍歷
由於 key.hashCode()函數調用的是 key 鍵值類型自帶的哈希函數,返回 int 型散列值。int 值範圍爲**-2147483648~2147483647**,先後加起來大概 40 億的映射空間。只要哈希函數映射得比較均勻鬆散,通常應用是很難出現碰撞的。但問題是一個 40 億長度的數組,內存是放不下的。
設想,若是 HashMap 數組的初始大小才 16,用以前須要對數組的長度取模運算,獲得的餘數才能用來訪問數組下標。
一、再哈希法:若是hash出的index已經有值,就再hash,不行繼續hash,直至找到空的index位置,要相信瞎貓總能碰上死耗子。這個辦法最容易想到。但有2個缺點:
比較浪費空間,消耗效率。根本緣由仍是數組的長度是固定不變的,不斷hash找出空的index,可能越界,這時就要建立新數組,而老數組的數據也須要遷移。隨着數組愈來愈大,消耗不可小覷。
get不到,或者說get算法複雜。進是進去了,想出來就沒那麼容易了。
二、開放地址方法:若是hash出的index已經有值,經過算法在它前面或後面的若干位置尋找空位,這個和再hash算法差異不大。
三、創建公共溢出區: 把衝突的hash值放到另一塊溢出區。
四、鏈式地址法: 把產生hash衝突的hash值以鏈表形式存儲在index位置上。HashMap用的就是該方法。優勢是不須要另外開闢新空間,也不會丟失數據,尋址也比較簡單。可是隨着hash鏈愈來愈長,尋址也是更加耗時。好的hash算法就是要讓鏈儘可能短,最好一個index上只有一個值。也就是儘量地保證散列地址分佈均勻,同時要計算簡單。
保證了對象的 hashCode 的 32 位值只要有一位發生改變,整個 hash() 返回值就會改變。儘量的減小碰撞。
①、table 數組大小是由 capacity 這個參數肯定的,默認是16,也能夠構造時傳入,最大限制是1<<30;
②、loadFactor 是裝載因子,主要目的是用來確認table 數組是否須要動態擴展,默認值是0.75,好比table 數組大小爲 16,裝載因子爲 0.75 時,threshold 就是12,當 table 的實際大小超過 12 時,table就須要動態擴容;
③、擴容時,調用 resize() 方法,將 table 長度變爲原來的兩倍(注意是 table 長度,而不是 threshold);
④、若是數據很大的狀況下,擴展時將會帶來性能的損失,在性能要求很高的地方,這種損失極可能很致命。
loadFactor表示HashMap的擁擠程度,影響hash操做到同一個數組位置的機率。
默認loadFactor等於0.75,當HashMap裏面容納的元素已經達到HashMap數組長度的75%時,表示HashMap太擠了,須要擴容,在HashMap的構造器中能夠定製loadFactor。
因爲JDK版本中HashMap設計上存在差別,這裏說說JDK7和JDK8中的區別:
具體put流程,請參照下圖進行回答:
由於紅黑樹的平均查找長度是log(n),長度爲8的時候,平均查找長度爲3,若是繼續使用鏈表,平均查找長度爲8/2=4,因此,當鏈表長度 >= 8時 ,有必要將鏈表轉換成紅黑樹。
容量爲32。
在HashMap中有個靜態方法tableSizeFor ,tableSizeFor方法保證函數返回值是大於等於給定參數initialCapacity最小的2的冪次方的數值 。
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n = MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
複製代碼
建立一個新的數組,其容量爲舊數組的兩倍,並從新計算舊數組中結點的存儲位置。結點在新數組中的位置只有兩種,原下標位置或原下標+舊數組的大小。
對key的hashCode進行hash值計算,與運算計算下標獲取bucket位置,若是在桶的首位上就能夠找到就直接返回,不然在樹中找或者鏈表中遍歷找,若是有hash衝突,則利用equals方法去遍歷鏈表查找節點。
之因此選擇紅黑樹是爲了解決二叉查找樹的缺陷,二叉查找樹在特殊狀況下會變成一條線性結構(這就跟原來使用鏈表結構同樣了,形成很深的問題),遍歷查找會很是慢。而紅黑樹在插入新數據後可能須要經過左旋,右旋、變色這些操做來保持平衡,引入紅黑樹就是爲了查找數據快,解決鏈表查詢深度的問題,咱們知道紅黑樹屬於平衡二叉樹,可是爲了保持「平衡」是須要付出代價的,可是該代價所損耗的資源要比遍歷線性鏈表要少,因此當長度大於8的時候,會使用紅黑樹,若是鏈表長度很短的話,根本不須要引入紅黑樹,引入反而會慢。
紅黑樹是一種自平衡的二叉查找樹,是一種高效的查找樹。
紅黑樹經過以下的性質定義實現自平衡:
節點是紅色或黑色。
根是黑色。
全部葉子都是黑色(葉子是NIL節點)。
每一個紅色節點必須有兩個黑色的子節點。(從每一個葉子到根的全部路徑上不能有兩個連續的紅色節點。)
從任一節點到其每一個葉子的全部簡單路徑都包含相同數目的黑色節點(簡稱黑高)。
1.在java 1.8中,若是鏈表的長度超過了8,那麼鏈表將轉換爲紅黑樹。(桶的數量必須大於64,小於64的時候只會擴容)
2.發生hash碰撞時,java 1.7 會在鏈表的頭部插入,而java 1.8會在鏈表的尾部插入
3.在java 1.8中,Entry被Node替代(換了一個馬甲)。
平時可能你們使用的最多的就是使用 String 做爲 HashMap 的 key,可是如今咱們想使用某個自定 義類做爲 HashMap 的 key,那就須要注意如下幾點:
若是類重寫了 equals 方法,它也應該重寫 hashCode 方法。
類的全部實例須要遵循與 equals 和 hashCode 相關的規則。
若是一個類沒有使用 equals,你不該該在 hashCode 中使用它。
我們自定義 key 類的最佳實踐是使之爲不可變的,這樣,hashCode 值能夠被緩存起來,擁有
更好的性能。不可變的類也能夠確保 hashCode 和 equals 在將來不會改變,這樣就會解決與可變相關的問題了。
爲了能讓 HashMap 存數據和取數據的效率高,儘量地減小 hash 值的碰撞,也就是說盡可能把數 據能均勻的分配,每一個鏈表或者紅黑樹長度儘可能相等。咱們首先可能會想到 % 取模的操做來實現。下面是回答的重點喲:
取餘(%)操做中若是除數是 2 的冪次,則等價於與其除數減一的與(&)操做(也就是說 hash % length == hash &(length - 1) 的前提是 length 是 2 的 n 次方)。而且,採用二進 制位操做 & ,相對於 % 可以提升運算效率。
這就是爲何 HashMap 的長度須要 2 的 N 次方了。
LinkedHashMap是繼承於HashMap,是基於HashMap和雙向鏈表來實現的。
HashMap無序;LinkedHashMap有序,可分爲插入順序和訪問順序兩種。若是是訪問順序,那put和get操做已存在的Entry時,都會把Entry移動到雙向鏈表的表尾(實際上是先刪除再插入)。
LinkedHashMap存取數據,仍是跟HashMap同樣使用的Entry[]的方式,雙向鏈表只是爲了保證順序。
LinkedHashMap是線程不安全的。
fail-fast 機制是 Java 集合(Collection)中的一種錯誤機制。當多個線程對同一個集合的內容進行 操做時,就可能會產生 fail-fast 事件。
例如:當某一個線程 A 經過 iterator 去遍歷某集合的過程當中,若該集合的內容被其餘線程所改變 了,那麼線程 A 訪問集合時,就會拋出 ConcurrentModificationException 異常,產生 fail-fast 事 件。這裏的操做主要是指 add、remove 和 clear,對集合元素個數進行修改。
解決辦法
建議使用「java.util.concurrent 包下的類」去取代「java.util 包下的類」。能夠這麼理解:在遍歷以前,把 modCount 記下來 expectModCount,後面 expectModCount 去 和 modCount 進行比較,若是不相等了,證實已併發了,被修改了,因而拋出 ConcurrentModificationException 異常。
①、HashMap 是線程不安全的,HashTable 是線程安全的;
②、因爲線程安全,因此 HashTable 的效率比不上 HashMap;
③、HashMap最多隻容許一條記錄的鍵爲null,容許多條記錄的值爲null,而 HashTable不容許;
④、HashMap 默認初始化數組的大小爲16,HashTable 爲 11,前者擴容時,擴大兩倍,後者擴大兩倍+1;
⑤、HashMap 須要從新計算 hash 值,而 HashTable 直接使用對象的 hashCode;
不是,在多線程環境下,1.7 會產生死循環、數據丟失、數據覆蓋的問題,1.8 中會有數據覆蓋的問題,以 1.8 爲例,當 A 線程判斷 index 位置爲空後正好掛起,B 線程開始往 index 位置的寫入節點數據,這時 A 線程恢復現場,執行賦值操做,就把 A 線程的數據給覆蓋了;還有++size 這個地方也會形成多線程同時擴容等問題。
單線程條件下,爲避免出現ConcurrentModificationException,須要保證只經過HashMap自己或者只經過Iterator去修改數據,不能在Iterator使用結束以前使用HashMap自己的方法修改數據。由於經過Iterator刪除數據時,HashMap的modCount和Iterator的expectedModCount都會自增,不影響兩者的相等性。若是是增長數據,只能經過HashMap自己的方法完成,此時若是要繼續遍歷數據,須要從新調用iterator()方法從而從新構造出一個新的Iterator,使得新Iterator的expectedModCount與更新後的HashMap的modCount相等。
多線程條件下,可以使用兩種方式:
Collections.synchronizedMap方法構造出一個同步Map
直接使用線程安全的ConcurrentHashMap。
都是 key-value 形式的存儲數據;
HashMap 是線程不安全的,ConcurrentHashMap 是 JUC 下的線程安全的;
HashMap 底層數據結構是數組 + 鏈表(JDK 1.8 以前)。JDK 1.8 以後是數組 + 鏈表 + 紅黑 樹。當鏈表中元素個數達到 8 的時候,鏈表的查詢速度不如紅黑樹快,鏈表會轉爲紅黑樹,紅 黑樹查詢速度快;
HashMap 初始數組大小爲 16(默認),當出現擴容的時候,以 0.75 * 數組大小的方式進行擴 容;
ConcurrentHashMap 在 JDK 1.8 以前是採用分段鎖來現實的 Segment + HashEntry, Segment 數組大小默認是 16,2 的 n 次方;JDK 1.8 以後,採用 Node + CAS + Synchronized 來保證併發安全進行實現。
HashTable:使用一把鎖(鎖住整個鏈表結構)處理併發問題,多個線程競爭一把鎖,容易阻塞;
ConcurrentHashMap:
JDK 1.7 中使用分段鎖(ReentrantLock + Segment + HashEntry
),至關於把一個 HashMap 分紅多個段,每段分配一把鎖,這樣支持多線程訪問。鎖粒度:基於 Segment,包含多個 HashEntry。
JDK 1.8 中使用CAS + synchronized + Node + 紅黑樹
。鎖粒度:Node(首結點)(實現 Map.Entry<K,V>)。鎖粒度下降了。
JDK 1.7 中,採用分段鎖的機制,實現併發的更新操做,底層採用數組+鏈表的存儲結構,包括兩個核心靜態內部類 Segment 和 HashEntry。
①、Segment 繼承 ReentrantLock(重入鎖) 用來充當鎖的角色,每一個 Segment 對象守護每一個散列映射表的若干個桶;
②、HashEntry 用來封裝映射表的鍵-值對;
③、每一個桶是由若干個 HashEntry 對象連接起來的鏈表
JDK 1.8 中,採用Node + CAS + Synchronized來保證併發安全。取消類 Segment,直接用 table 數組存儲鍵值對;當 HashEntry 對象組成的鏈表長度超過 TREEIFY_THRESHOLD 時,鏈表轉換爲紅黑樹,提高性能。底層變動爲數組 + 鏈表 + 紅黑樹。
①、粒度下降了;
②、JVM 開發團隊沒有放棄 synchronized,並且基於 JVM 的 synchronized 優化空間更大,更加天然。
③、在大量的數據操做下,對於 JVM 的內存壓力,基於 API 的 ReentrantLock 會開銷更多的內存。
①、重要的常量:
private transient volatile int sizeCtl;
當爲負數時,-1 表示正在初始化,-N 表示 N - 1 個線程正在進行擴容;
當爲 0 時,表示 table 尚未初始化;
當爲其餘正數時,表示初始化或者下一次進行擴容的大小。
②、數據結構:
Node 是存儲結構的基本單元,繼承 HashMap 中的 Entry,用於存儲數據;
TreeNode 繼承 Node,可是數據結構換成了二叉樹結構,是紅黑樹的存儲結構,用於紅黑樹中存儲數據;
TreeBin 是封裝 TreeNode 的容器,提供轉換紅黑樹的一些條件和鎖的控制。
③、存儲對象時(put() 方法):
若是沒有初始化,就調用 initTable() 方法來進行初始化;
若是沒有 hash 衝突就直接 CAS 無鎖插入;
若是須要擴容,就先進行擴容;
若是存在 hash 衝突,就加鎖來保證線程安全,兩種狀況:一種是鏈表形式就直接遍歷到尾端插入,一種是紅黑樹就按照紅黑樹結構插入;
若是該鏈表的數量大於閥值 8,就要先轉換成紅黑樹的結構,break 再一次進入循環
若是添加成功就調用 addCount() 方法統計 size,而且檢查是否須要擴容。
④、擴容方法 transfer():默認容量爲 16,擴容時,容量變爲原來的兩倍。 helpTransfer():調用多個工做線程一塊兒幫助進行擴容,這樣的效率就會更高。
⑤、獲取對象時(get()方法):
計算 hash 值,定位到該 table 索引位置,若是是首結點符合就返回;
若是遇到擴容時,會調用標記正在擴容結點 ForwardingNode.find()方法,查找該結點,匹配就返回;
以上都不符合的話,就往下遍歷結點,匹配就返回,不然最後就返回 null。
程序運行時可以同時更新 ConccurentHashMap 且不產生鎖競爭的最大線程數。默認爲 16,且能夠在構造函數中設置。當用戶設置併發度時,ConcurrentHashMap 會使用大於等於該值的最小2冪指數做爲實際併發度(假如用戶設置併發度爲17,實際併發度則爲32)。
好了,就寫這麼多了,文章中不少已經不是HashMap知識點了,但,面試頗有可能會問這些知識點,多準備點也算是有備無患。
所謂天才,只不過是把別人喝咖啡的功夫都用在工做上了。
——魯迅
推薦閱讀
期待你的轉發、點贊,在看,謝啦
本文使用 文章同步助手 同步