你知道的越多,你不知道的越多java
點贊再看,養成習慣node
本文 GitHub https://github.com/JavaFamily 已收錄,有一線大廠面試點思惟導圖,也整理了不少個人文檔,歡迎Star和完善,你們面試能夠參照考點複習,但願咱們一塊兒有點東西。git
做爲一個在互聯網公司面一次拿一次Offer的麪霸,戰勝了無數競爭對手,每次都只能看到無數落寞的身影失望的離開,略感愧疚(請容許我使用一下誇張的修辭手法)。github
因而在一個寂寞難耐的夜晚,我痛定思痛,決定開始寫互聯網技術棧面試相關的文章,但願能幫助各位讀者之後面試勢如破竹,對面試官進行360°的反擊,吊打問你的面試官,讓一同面試的同僚瞠目結舌,瘋狂收割大廠Offer!面試
全部文章的名字只是個人噱頭,咱們應該有一顆謙遜的心,因此但願你們懷着空杯心態好好學,一塊兒進步。sql
上次面試呀,我發現面試官對個人幾個回答仍是不夠滿意,以爲仍是有點疑問,我就挑幾個回答一下。數據庫
16是2的冪,8也是,32也是,爲啥恰恰選了16?編程
我以爲就是一個經驗值,定義16沒有很特別的緣由,只要是2次冪,其實用 8 和 32 都差很少。數組
用16只是由於做者認爲16這個初始容量是能符合經常使用而已。安全
Hashmap中的鏈表大小超過八個時會自動轉化爲紅黑樹,當刪除小於六時從新變爲鏈表,爲啥呢?
根據泊松分佈,在負載因子默認爲0.75的時候,單個hash槽內元素個數爲8的機率小於百萬分之一,因此將7做爲一個分水嶺,等於7的時候不轉換,大於等於8的時候才進行轉換,小於等於6的時候就化爲鏈表。
一個婀娜多姿,穿着襯衣的小姐姐,拿着一個精緻的小筆記本,徑直走過來坐在個人面前。
就在我口水要都要流出來的時候,小姐姐的話語打斷了個人YY。
喂小鬼,你養我啊!
呸呸呸,說錯了,上次的HashMap回答得不錯,最後由於天色太晚了面試草草收場,此次可得好好安排你。
誒,面試官上次是在抱歉,由於公司雙十二要值班,實在是沒辦法,不過此次不會了,我推掉了全部的事情準備全身心投入到今天的面試中,甚至推掉了隔壁王大爺的約會邀約。
這樣最好,上次咱們最後聊到HashMap在多線程環境下存在線程安全問題,那你通常都是怎麼處理這種狀況的?
美麗迷人的面試官您好,通常在多線程的場景,我都會使用好幾種不一樣的方式去代替:
不過出於線程併發度的緣由,我都會捨棄前二者使用最後的ConcurrentHashMap,他的性能和效率明顯高於前二者。
哦,Collections.synchronizedMap是怎麼實現線程安全的你有了解過麼?
臥*!不按照套路出牌呀,正常不都是問HashMap和ConcurrentHashMap麼,此次怎麼問了這個鬼東西,還好我飽讀詩書,常常看敖丙的《吊打面試官》系列,否則真的完了。
小姐姐您這個問題真好,別的面試官都沒問過,說真的您水平確定是頂級技術專家吧。
別貧嘴,快回答個人問題!抿嘴一笑😁
在SynchronizedMap內部維護了一個普通對象Map,還有排斥鎖mutex,如圖
Collections.synchronizedMap(new HashMap<>(16));
咱們在調用這個方法的時候就須要傳入一個Map,能夠看到有兩個構造器,若是你傳入了mutex參數,則將對象排斥鎖賦值爲傳入的對象。
若是沒有,則將對象排斥鎖賦值爲this,即調用synchronizedMap的對象,就是上面的Map。
建立出synchronizedMap以後,再操做map的時候,就會對方法上鎖,如圖全是🔐
臥*,小夥子,秒啊,其實我早就忘了源碼了,就是瞎問一下,沒想到仍是回答上來了,接下來就面對疾風吧。
回答得不錯,能跟我聊一下Hashtable麼?
這個我就等着你問呢嘿嘿!
跟HashMap相比Hashtable是線程安全的,適合在多線程的狀況下使用,可是效率可不太樂觀。
哦,你能說說他效率低的緣由麼?
嗯嗯面試官,我看過他的源碼,他在對數據操做的時候都會上鎖,因此效率比較低下。
除了這個你還能說出一些Hashtable 跟HashMap不同點麼?
!吶呢?這叫什麼問題嘛?這個又是知識盲區呀!
呃,面試官我歷來沒使用過他,你容我想一想區別的點,說完便開始抓頭髮,此次不是裝的,是真的!
Hashtable 是不容許鍵或值爲 null 的,HashMap 的鍵值則均可覺得 null。
呃我能打斷你一下麼?爲啥 Hashtable 是不容許 KEY 和 VALUE 爲 null, 而 HashMap 則能夠呢?
尼*,我這個時候怎麼以爲面前的人很差看了,甚至像個魔鬼,看着對本身面試官內心想到。
由於Hashtable在咱們put 空值的時候會直接拋空指針異常,可是HashMap卻作了特殊處理。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
可是你仍是沒說爲啥Hashtable 是不容許鍵或值爲 null 的,HashMap 的鍵值則均可覺得 null?
這是由於Hashtable使用的是安全失敗機制(fail-safe),這種機制會使你這次讀到的數據不必定是最新的數據。
若是你使用null值,就會使得其沒法判斷對應的key是不存在仍是爲空,由於你沒法再調用一次contain(key)來對key是否存在進行判斷,ConcurrentHashMap同理。
好的你繼續說不一樣點吧。
實現方式不一樣:Hashtable 繼承了 Dictionary類,而 HashMap 繼承的是 AbstractMap 類。
Dictionary 是 JDK 1.0 添加的,貌似沒人用過這個,我也沒用過。
初始化容量不一樣:HashMap 的初始容量爲:16,Hashtable 初始容量爲:11,二者的負載因子默認都是:0.75。
擴容機制不一樣:當現有容量大於總容量 * 負載因子時,HashMap 擴容規則爲當前容量翻倍,Hashtable 擴容規則爲當前容量翻倍 + 1。
迭代器不一樣:HashMap 中的 Iterator 迭代器是 fail-fast 的,而 Hashtable 的 Enumerator 不是 fail-fast 的。
因此,當其餘線程改變了HashMap 的結構,如:增長、刪除元素,將會拋出ConcurrentModificationException 異常,而 Hashtable 則不會。
fail-fast是啥?
臥*,你本身不知道麼?爲啥問我!!!還好我會!
快速失敗(fail—fast)是java集合中的一種機制, 在用迭代器遍歷一個集合對象時,若是遍歷過程當中對集合對象的內容進行了修改(增長、刪除、修改),則會拋出Concurrent Modification Exception。
他的原理是啥?
迭代器在遍歷時直接訪問集合中的內容,而且在遍歷過程當中使用一個 modCount 變量。
集合在被遍歷期間若是內容發生變化,就會改變modCount的值。
每當迭代器使用hashNext()/next()遍歷下一個元素以前,都會檢測modCount變量是否爲expectedmodCount值,是的話就返回遍歷;不然拋出異常,終止遍歷。
Tip:這裏異常的拋出條件是檢測到 modCount!=expectedmodCount 這個條件。若是集合發生變化時修改modCount值恰好又設置爲了expectedmodCount值,則異常不會拋出。
所以,不能依賴於這個異常是否拋出而進行併發操做的編程,這個異常只建議用於檢測併發修改的bug。
說說他的場景?
java.util包下的集合類都是快速失敗的,不能在多線程下發生併發修改(迭代過程當中被修改)算是一種安全機制吧。
Tip:安全失敗(fail—safe)你們也能夠了解下,java.util.concurrent包下的容器都是安全失敗,能夠在多線程下併發使用,併發修改。
嗯?這個小鬼這麼有東西的嘛?竟然把不一樣點幾乎都說出來了,被人遺忘的Hashtable都能說得頭頭是道,看來不簡單,不知道接下來的ConcurrentHashMap連環炮能不能頂得住了。
都說了他的併發度不夠,性能很低,這個時候你都怎麼處理的?
他來了他來了,他終於仍是來了,等了這麼久,就是等你問我這個點,你仍是掉入了個人陷阱啊,我早有準備,在HashMap埋下他線程不安全的種子,就是爲了在ConcurrentHashMap開花結果!
小姐姐:這樣的場景,咱們在開發過程當中都是使用ConcurrentHashMap,他的併發的相比前二者好不少。
哦?那你跟我說說他的數據結構吧,以及爲啥他併發度這麼高?
ConcurrentHashMap 底層是基於 數組 + 鏈表
組成的,不過在 jdk1.7 和 1.8 中具體實現稍有不一樣。
我先說一下他在1.7中的數據結構吧:
如圖所示,是由 Segment 數組、HashEntry 組成,和 HashMap 同樣,仍然是數組加鏈表。
Segment 是 ConcurrentHashMap 的一個內部類,主要的組成以下:
static final class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
// 和 HashMap 中的 HashEntry 做用同樣,真正存放數據的桶
transient volatile HashEntry<K,V>[] table;
transient int count;
// 記得快速失敗(fail—fast)麼?
transient int modCount;
// 大小
transient int threshold;
// 負載因子
final float loadFactor;
}
HashEntry跟HashMap差很少的,可是不一樣點是,他使用volatile去修飾了他的數據Value還有下一個節點next。
volatile的特性是啥?
保證了不一樣線程對這個變量進行操做時的可見性,即一個線程修改了某個變量的值,這新值對其餘線程來講是當即可見的。(實現可見性)
禁止進行指令重排序。(實現有序性)
volatile 只能保證對單次讀/寫的原子性。i++ 這種操做不能保證原子性。
我就不大篇幅介紹了,多線程章節我會說到的,你們知道用了以後安全了就對了。
那你能說說他併發度高的緣由麼?
原理上來講,ConcurrentHashMap 採用了分段鎖技術,其中 Segment 繼承於 ReentrantLock。
不會像 HashTable 那樣無論是 put 仍是 get 操做都須要作同步處理,理論上 ConcurrentHashMap 支持 CurrencyLevel (Segment 數組數量)的線程併發。
每當一個線程佔用鎖訪問一個 Segment 時,不會影響到其餘的 Segment。
就是說若是容量大小是16他的併發度就是16,能夠同時容許16個線程操做16個Segment並且仍是線程安全的。
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();//這就是爲啥他不能夠put null值的緣由
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null)
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
他先定位到Segment,而後再進行put操做。
咱們看看他的put源代碼,你就知道他是怎麼作到線程安全的了,關鍵句子我註釋了。
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 將當前 Segment 中的 table 經過 key 的 hashcode 定位到 HashEntry
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
// 遍歷該 HashEntry,若是不爲空則判斷傳入的 key 和當前遍歷的 key 是否相等,相等則覆蓋舊的 value。
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
// 不爲空則須要新建一個 HashEntry 並加入到 Segment 中,同時會先判斷是否須要擴容。
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
//釋放鎖
unlock();
}
return oldValue;
}
首先第一步的時候會嘗試獲取鎖,若是獲取失敗確定就有其餘線程存在競爭,則利用 scanAndLockForPut()
自旋獲取鎖。
MAX_SCAN_RETRIES
則改成阻塞鎖獲取,保證能獲取成功。那他get的邏輯呢?
get 邏輯比較簡單,只須要將 Key 經過 Hash 以後定位到具體的 Segment ,再經過一次 Hash 定位到具體的元素上。
因爲 HashEntry 中的 value 屬性是用 volatile 關鍵詞修飾的,保證了內存可見性,因此每次獲取時都是最新值。
ConcurrentHashMap 的 get 方法是很是高效的,由於整個過程都不須要加鎖。
你有沒有發現1.7雖然能夠支持每一個Segment併發訪問,可是仍是存在一些問題?
是的,由於基本上仍是數組加鏈表的方式,咱們去查詢的時候,還得遍歷鏈表,會致使效率很低,這個跟jdk1.7的HashMap是存在的同樣問題,因此他在jdk1.8徹底優化了。
那你再跟我聊聊jdk1.8他的數據結構是怎麼樣子的呢?
其中拋棄了原有的 Segment 分段鎖,而採用了 CAS + synchronized
來保證併發安全性。
跟HashMap很像,也把以前的HashEntry改爲了Node,可是做用不變,把值和next採用了volatile去修飾,保證了可見性,而且也引入了紅黑樹,在鏈表大於必定值的時候會轉換(默認是8)。
一樣的,你能跟我聊一下他值的存取操做麼?以及是怎麼保證線程安全的?
ConcurrentHashMap在進行put操做的仍是比較複雜的,大體能夠分爲如下步驟:
hashcode == MOVED == -1
,則須要進行擴容。TREEIFY_THRESHOLD
則要轉換爲紅黑樹。你在上面提到CAS是什麼?自旋又是什麼?
CAS 是樂觀鎖的一種實現方式,是一種輕量級鎖,JUC 中不少工具類的實現就是基於 CAS 的。
CAS 操做的流程以下圖所示,線程在讀取數據時不進行加鎖,在準備寫回數據時,比較原值是否修改,若未被其餘線程修改則寫回,若已被修改,則從新執行讀取流程。
這是一種樂觀策略,認爲併發操做並不總會發生。
仍是不明白?那我再說明下,樂觀鎖在實際開發場景中很是常見,你們仍是要去理解。
就好比我如今要修改數據庫的一條數據,修改以前我先拿到他原來的值,而後在SQL裏面還會加個判斷,原來的值和我手上拿到的他的原來的值是否同樣,同樣咱們就能夠去修改了,不同就證實被別的線程修改了你就return錯誤就行了。
SQL僞代碼大概以下:
update a set value = newValue where value = #{oldValue}//oldValue就是咱們執行前查詢出來的值
CAS就必定能保證數據沒被別的線程修改過麼?
並非的,好比很經典的ABA問題,CAS就沒法判斷了。
什麼是ABA?
就是說來了一個線程把值改回了B,又來了一個線程把值又改回了A,對於這個時候判斷的線程,就發現他的值仍是A,因此他就不知道這個值到底有沒有被人改過,其實不少場景若是隻追求最後結果正確,這是不要緊的。
可是實際過程當中仍是須要記錄修改過程的,好比資金修改什麼的,你每次修改的都應該有記錄,方便回溯。
那怎麼解決ABA問題?
用版本號去保證就行了,就好比說,我在修改前去查詢他原來的值的時候再帶一個版本號,每次判斷就連值和版本號一塊兒判斷,判斷成功就給版本號加1。
update a set value = newValue ,vision = vision + 1 where value = #{oldValue} and vision = #{vision} // 判斷原來的值和版本號是否匹配,中間有別的線程修改,值可能相等,可是版本號100%不同
牛*,有點東西,除了版本號還有別的方法保證麼?
其實有不少方式,好比時間戳也能夠,查詢的時候把時間戳一塊兒查出來,對的上才修改而且更新值的時候一塊兒修改更新時間,這樣也能保證,方法不少可是跟版本號都是殊途同歸之妙,看場景你們想怎麼設計吧。
CAS性能很高,可是我知道synchronized性能可不咋地,爲啥jdk1.8升級以後反而多了synchronized?
synchronized以前一直都是重量級的鎖,可是後來java官方是對他進行過升級的,他如今採用的是鎖升級的方式去作的。
針對 synchronized 獲取鎖的方式,JVM 使用了鎖升級的優化方式,就是先使用偏向鎖優先同一線程而後再次獲取鎖,若是失敗,就升級爲 CAS 輕量級鎖,若是失敗就會短暫自旋,防止線程被系統掛起。最後若是以上都失敗就升級爲重量級鎖。
因此是一步步升級上去的,最初也是經過不少輕量級的方式鎖定的。
🐂,那咱們迴歸正題,ConcurrentHashMap的get操做又是怎麼樣子的呢?
小結:1.8 在 1.7 的數據結構上作了大的改動,採用紅黑樹以後能夠保證查詢效率(O(logn)
),甚至取消了 ReentrantLock 改成了 synchronized,這樣能夠看出在新版的 JDK 中對 synchronized 優化是很到位的。
Hashtable&ConcurrentHashMap跟HashMap基本上就是一套連環組合,我在面試的時候常常能吹上好久,常常被面試官說:好了好了,咱們繼續下一個話題吧哈哈。
是的由於提到HashMap你確定會聊到他的線程安全性這一點,那你總不能加鎖一句話就搞定了吧,java的做者們也不想,因此人家寫開發了對應的替代品,那就是線程安全的Hashtable&ConcurrentHashMap。
二者都有特色,可是線程安全場景仍是後者用得多一點,緣由我在文中已經大篇幅全方位的介紹了,這裏就再也不過多贅述了。
大家發現了面試就是一個個的坑,你說到啥面試官可能就懟到你啥,別問我爲啥知道嘿嘿。
你知道不肯定能不能爲這場面試加分,可是不知道確定是減分的,文中的快速失敗(fail—fast)問到,那對應的安全失敗(fail—safe)也是有可能知道的,我想讀者不少都不知道吧,由於我問過不少仔哈哈。
還有提到CAS樂觀鎖,你要知道ABA,你要知道解決方案,由於在實際的開發場景真的不要太經常使用了,sync的鎖升級你也要知道。
我沒過多描述線程安全的太多東西,由於我都寫了,之後更啥?對吧哈哈。
在回答Hashtable和ConcurrentHashMap相關的面試題的時候,必定要知道他們是怎麼保證線程安全的,那線程不安全通常都是發生在存取的過程當中的,那get、put你確定要知道。
HashMap是必問的那種,這兩個常常會做爲替補問題,不過也常常問,他們自己的機制其實都比較簡單,特別是ConcurrentHashMap跟HashMap是很像的,只是是否線程安全這點不一樣。
提到線程安全那你就要知道相關的知識點了,好比說到CAS你必定要知道ABA的問題,提到synchronized那你要知道他的原理,他鎖對象,方法、代碼塊,在底層是怎麼實現的。
synchronized你還須要知道他的鎖升級機制,以及他的兄弟ReentantLock,二者一個是jvm層面的一個是jdk層面的,仍是有很大的區別的。
那提到他們兩個你是否是又須要知道juc這個包下面的全部的經常使用類,以及他們的底層原理了?
那提到……
好了各位,以上就是這篇文章的所有內容了,能看到這裏的人呀,都是人才。
我後面會每週都更新幾篇一線互聯網大廠面試和經常使用技術棧相關的文章,很是感謝人才們能看到這裏,若是這個文章寫得還不錯,以爲「敖丙」我有點東西的話 求點贊👍 求關注❤️ 求分享👥 對暖男我來講真的 很是有用!!!
白嫖很差,創做不易,各位的支持和承認,就是我創做的最大動力,咱們下篇文章見!
敖丙 | 文 【原創】
若是本篇博客有任何錯誤,請批評指教,不勝感激 !
文章每週持續更新,能夠微信搜索「 三太子敖丙 」第一時間閱讀和催更(比博客早一到兩篇喲),本文 GitHub https://github.com/JavaFamily 已經收錄,有一線大廠面試點思惟導圖,也整理了不少個人文檔,歡迎Star和完善,你們面試能夠參照考點複習,但願咱們一塊兒有點東西。