本文彙總了常考的 ConcurrentHashMap 面試題,面試 ConcurrentHashMap,看這一篇就夠了!爲幫助你們高效複習,專門用」★ 「表示面試中出現的頻率,」★ 「越多,表明越高頻!面試
ConcurrentHashMap 的實現原理是什麼?★★★★★算法
ConcurrentHashMap 在 JDK1.7 和 JDK1.8 的實現方式是不一樣的。數組
先來看下JDK1.7安全
JDK1.7 中的 ConcurrentHashMap 是由 Segment 數組結構和 HashEntry 數組結構組成,即 ConcurrentHashMap 把哈希桶數組切分紅小數組(Segment ),每一個小數組有 n 個 HashEntry 組成。數據結構
以下圖所示,首先將數據分爲一段一段的存儲,而後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一段數據時,其餘段的數據也能被其餘線程訪問,實現了真正的併發訪問。多線程
Segment 是 ConcurrentHashMap 的一個內部類,主要的組成以下:併發
Segment 繼承了 ReentrantLock,因此 Segment 是一種可重入鎖,扮演鎖的角色。Segment 默認爲 16,也就是併發度爲 16。ide
存放元素的 HashEntry,也是一個靜態內部類,主要的組成以下:函數
其中,用 volatile 修飾了 HashEntry 的數據 value 和 下一個節點 next,保證了多線程環境下數據獲取時的可見性!性能
再來看下JDK1.8
在數據結構上, JDK1.8 中的ConcurrentHashMap 選擇了與 HashMap 相同的Node數組+鏈表+紅黑樹結構;在鎖的實現上,拋棄了原有的 Segment 分段鎖,採用CAS + synchronized實現更加細粒度的鎖。
將鎖的級別控制在了更細粒度的哈希桶數組元素級別,也就是說只須要鎖住這個鏈表頭節點(紅黑樹的根節點),就不會影響其餘的哈希桶數組元素的讀寫,大大提升了併發度。
JDK1.8 中爲何使用內置鎖 synchronized替換 可重入鎖 ReentrantLock?★★★★★
ConcurrentHashMap 的 put 方法執行邏輯是什麼?★★★★
先來看JDK1.7
先定位到相應的 Segment ,而後再進行 put 操做。
源代碼以下:
首先會嘗試獲取鎖,若是獲取失敗確定就有其餘線程存在競爭,則利用 scanAndLockForPut() 自旋獲取鎖。
再來看JDK1.8
大體能夠分爲如下步驟:
根據 key 計算出 hash 值;
判斷是否須要進行初始化;
定位到 Node,拿到首節點 f,判斷首節點 f:
當在鏈表長度達到 8 的時候,數組擴容或者將鏈表轉換爲紅黑樹。
源代碼以下:
ConcurrentHashMap 的 get 方法執行邏輯是什麼?★★★★
一樣,先來看JDK1.7
首先,根據 key 計算出 hash 值定位到具體的 Segment ,再根據 hash 值獲取定位 HashEntry 對象,並對 HashEntry 對象進行鏈表遍歷,找到對應元素。
因爲 HashEntry 涉及到的共享變量都使用 volatile 修飾,volatile 能夠保證內存可見性,因此每次獲取時都是最新值。
源代碼以下:
再來看JDK1.8
大體能夠分爲如下步驟:
根據 key 計算出 hash 值,判斷數組是否爲空;
若是是首節點,就直接返回;
若是是紅黑樹結構,就從紅黑樹裏面查詢;
若是是鏈表結構,循環遍歷判斷。
源代碼以下:
ConcurrentHashMap 的 get 方法是否要加鎖,爲何?★★★
get 方法不須要加鎖。由於 Node 的元素 value 和指針 next 是用 volatile 修飾的,在多線程環境下線程A修改節點的 value 或者新增節點的時候是對線程B可見的。
這也是它比其餘併發集合好比 Hashtable、用 Collections.synchronizedMap()包裝的 HashMap 效率高的緣由之一。
get 方法不須要加鎖與 volatile 修飾的哈希桶數組有關嗎?★★★
沒有關係。哈希桶數組table用 volatile 修飾主要是保證在數組擴容的時候保證可見性。
ConcurrentHashMap 不支持 key 或者 value 爲 null 的緣由?★★★
咱們先來講value 爲何不能爲 null。由於 ConcurrentHashMap 是用於多線程的 ,若是ConcurrentHashMap.get(key)獲得了 null ,這就沒法判斷,是映射的value是 null ,仍是沒有找到對應的key而爲 null ,就有了二義性。
而用於單線程狀態的 HashMap 卻能夠用containsKey(key) 去判斷究竟是否包含了這個 null 。
咱們用反證法來推理:
假設 ConcurrentHashMap 容許存放值爲 null 的 value,這時有A、B兩個線程,線程A調用ConcurrentHashMap.get(key)方法,返回爲 null ,咱們不知道這個 null 是沒有映射的 null ,仍是存的值就是 null 。
假設此時,返回爲 null 的真實狀況是沒有找到對應的 key。那麼,咱們能夠用 ConcurrentHashMap.containsKey(key)來驗證咱們的假設是否成立,咱們指望的結果是返回 false 。
可是在咱們調用 ConcurrentHashMap.get(key)方法以後,containsKey方法以前,線程B執行了ConcurrentHashMap.put(key, null)的操做。那麼咱們調用containsKey方法返回的就是 true 了,這就與咱們的假設的真實狀況不符合了,這就有了二義性。
至於 ConcurrentHashMap 中的 key 爲何也不能爲 null 的問題,源碼就是這樣寫的,哈哈。若是面試官不滿意,就回答由於做者Doug不喜歡 null ,因此在設計之初就不容許了 null 的 key 存在。想要深刻了解的小夥伴,能夠看這篇文章這道面試題我真不知道面試官想要的回答是什麼
ConcurrentHashMap 的併發度是什麼?★★
併發度能夠理解爲程序運行時可以同時更新 ConccurentHashMap且不產生鎖競爭的最大線程數。在JDK1.7中,實際上就是ConcurrentHashMap中的分段鎖個數,即Segment[]的數組長度,默認是16,這個值能夠在構造函數中設置。
若是本身設置了併發度,ConcurrentHashMap 會使用大於等於該值的最小的2的冪指數做爲實際併發度,也就是好比你設置的值是17,那麼實際併發度是32。
若是併發度設置的太小,會帶來嚴重的鎖競爭問題;若是併發度設置的過大,本來位於同一個Segment內的訪問會擴散到不一樣的Segment中,CPU cache命中率會降低,從而引發程序性能降低。
在JDK1.8中,已經摒棄了Segment的概念,選擇了Node數組+鏈表+紅黑樹結構,併發度大小依賴於數組的大小。
ConcurrentHashMap 迭代器是強一致性仍是弱一致性?★★
與 HashMap 迭代器是強一致性不一樣,ConcurrentHashMap 迭代器是弱一致性。
ConcurrentHashMap 的迭代器建立後,就會按照哈希表結構遍歷每一個元素,但在遍歷過程當中,內部元素可能會發生變化,若是變化發生在已遍歷過的部分,迭代器就不會反映出來,而若是變化發生在未遍歷過的部分,迭代器就會發現並反映出來,這就是弱一致性。
這樣迭代器線程可使用原來老的數據,而寫線程也能夠併發的完成改變,更重要的,這保證了多個線程併發執行的連續性和擴展性,是性能提高的關鍵。想要深刻了解的小夥伴,能夠看這篇文章:http://ifeve.com/ConcurrentHashMap-weakly-consistent/
JDK1.7 與 JDK1.8 中ConcurrentHashMap 的區別?★★★★★
ConcurrentHashMap 和 Hashtable 的效率哪一個更高?爲何?★★★★★
ConcurrentHashMap 的效率要高於 Hashtable,由於 Hashtable 給整個哈希表加了一把大鎖從而實現線程安全。而ConcurrentHashMap 的鎖粒度更低,在 JDK1.7 中採用分段鎖實現線程安全,在 JDK1.8 中採用CAS+synchronized實現線程安全。
具體說一下Hashtable的鎖機制 ★★★★★
Hashtable 是使用 synchronized來實現線程安全的,給整個哈希表加了一把大鎖,多線程訪問時候,只要有一個線程訪問或操做該對象,那其餘線程只能阻塞等待須要的鎖被釋放,在競爭激烈的多線程場景中性能就會很是差!
多線程下安全的操做 map還有其餘方法嗎?★★★
還可使用Collections.synchronizedMap方法,對方法進行加同步鎖。
若是傳入的是 HashMap 對象,其實也是對 HashMap 作的方法作了一層包裝,裏面使用對象鎖來保證多線程場景下,線程安全,本質也是對 HashMap 進行全表鎖。在競爭激烈的多線程環境下性能依然也很是差,不推薦使用!