【181期】HashMap 面試二十一問!
閱讀本文大概需要 8 分鐘。
來自:cnblogs.com/Young111/p/11519952.html
transient Node<K,V>[] table;
(JDK 1.7 之前使用頭插法、JDK 1.8 使用尾插法)
(注意:當(dāng)碰撞導(dǎo)致鏈表大于 TREEIFY_THRESHOLD = 8 時(shí),就把鏈表轉(zhuǎn)換成紅黑樹)
保證了對象的 hashCode 的 32 位值只要有一位發(fā)生改變,整個(gè) hash() 返回值就會(huì)改變。盡可能的減少碰撞。
table 數(shù)組大小是由 capacity 這個(gè)參數(shù)確定的,默認(rèn)是16,也可以構(gòu)造時(shí)傳入,最大限制是1<<30; loadFactor 是裝載因子,主要目的是用來確認(rèn)table 數(shù)組是否需要?jiǎng)討B(tài)擴(kuò)展,默認(rèn)值是0.75,比如table 數(shù)組大小為 16,裝載因子為 0.75 時(shí),threshold 就是12,當(dāng) table 的實(shí)際大小超過 12 時(shí),table就需要?jiǎng)討B(tài)擴(kuò)容; 擴(kuò)容時(shí),調(diào)用 resize() 方法,將 table 長度變?yōu)樵瓉淼膬杀叮ㄗ⒁馐?nbsp;table 長度,而不是 threshold) 如果數(shù)據(jù)很大的情況下,擴(kuò)展時(shí)將會(huì)帶來性能的損失,在性能要求很高的地方,這種損失很可能很致命。
如果沒有出現(xiàn)哈希沖突,則直接放入數(shù)組;如果出現(xiàn)哈希沖突,則以鏈表的方式放在鏈表后面; 如果鏈表長度超過閥值( TREEIFY THRESHOLD==8),就把鏈表轉(zhuǎn)成紅黑樹,鏈表長度低于6,就把紅黑樹轉(zhuǎn)回鏈表; 如果結(jié)點(diǎn)的key已經(jīng)存在,則替換其value即可; 如果集合中的鍵值對大于12,調(diào)用resize方法進(jìn)行數(shù)組擴(kuò)容?!?/section>
HashMap 參考其他問題; LinkedHashMap 保存了記錄的插入順序,在用 Iterator 遍歷時(shí),先取到的記錄肯定是先插入的;遍歷比 HashMap 慢; TreeMap 實(shí)現(xiàn) SortMap 接口,能夠把它保存的記錄根據(jù)鍵排序(默認(rèn)按鍵值升序排序,也可以指定排序的比較器)
HashMap:在 Map 中插入、刪除和定位元素時(shí); TreeMap:在需要按自然順序或自定義順序遍歷鍵的情況下; LinkedHashMap:在需要輸出的順序和輸入的順序相同的情況下。
HashMap 是線程不安全的,HashTable 是線程安全的; 由于線程安全,所以 HashTable 的效率比不上 HashMap; HashMap最多只允許一條記錄的鍵為null,允許多條記錄的值為null,而 HashTable不允許; HashMap 默認(rèn)初始化數(shù)組的大小為16,HashTable 為 11,前者擴(kuò)容時(shí),擴(kuò)大兩倍,后者擴(kuò)大兩倍+1; HashMap 需要重新計(jì)算 hash 值,而 HashTable 直接使用對象的 hashCode
JDK 1.7 中使用分段鎖(ReentrantLock + Segment + HashEntry),相當(dāng)于把一個(gè) HashMap 分成多個(gè)段,每段分配一把鎖,這樣支持多線程訪問。鎖粒度:基于 Segment,包含多個(gè) HashEntry。
JDK 1.8 中使用 CAS + synchronized + Node + 紅黑樹。鎖粒度:Node(首結(jié)點(diǎn))(實(shí)現(xiàn) Map.Entry<K,V>)。鎖粒度降低了。
Segment 繼承 ReentrantLock(重入鎖) 用來充當(dāng)鎖的角色,每個(gè) Segment 對象守護(hù)每個(gè)散列映射表的若干個(gè)桶; HashEntry 用來封裝映射表的鍵-值對; 每個(gè)桶是由若干個(gè) HashEntry 對象鏈接起來的鏈表


粒度降低了; JVM 開發(fā)團(tuán)隊(duì)沒有放棄 synchronized,而且基于 JVM 的 synchronized 優(yōu)化空間更大,更加自然。 在大量的數(shù)據(jù)操作下,對于 JVM 的內(nèi)存壓力,基于 API 的 ReentrantLock 會(huì)開銷更多的內(nèi)存。
private transient volatile int sizeCtl; 當(dāng)為負(fù)數(shù)時(shí),-1 表示正在初始化,-N 表示 N - 1 個(gè)線程正在進(jìn)行擴(kuò)容; 當(dāng)為 0 時(shí),表示 table 還沒有初始化; 當(dāng)為其他正數(shù)時(shí),表示初始化或者下一次進(jìn)行擴(kuò)容的大小。
Node 是存儲(chǔ)結(jié)構(gòu)的基本單元,繼承 HashMap 中的 Entry,用于存儲(chǔ)數(shù)據(jù); TreeNode 繼承 Node,但是數(shù)據(jù)結(jié)構(gòu)換成了二叉樹結(jié)構(gòu),是紅黑樹的存儲(chǔ)結(jié)構(gòu),用于紅黑樹中存儲(chǔ)數(shù)據(jù); TreeBin 是封裝 TreeNode 的容器,提供轉(zhuǎn)換紅黑樹的一些條件和鎖的控制。
如果沒有初始化,就調(diào)用 initTable() 方法來進(jìn)行初始化; 如果沒有 hash 沖突就直接 CAS 無鎖插入; 如果需要擴(kuò)容,就先進(jìn)行擴(kuò)容; 如果存在 hash 沖突,就加鎖來保證線程安全,兩種情況:一種是鏈表形式就直接遍歷到尾端插入,一種是紅黑樹就按照紅黑樹結(jié)構(gòu)插入; 如果該鏈表的數(shù)量大于閥值 8,就要先轉(zhuǎn)換成紅黑樹的結(jié)構(gòu),break 再一次進(jìn)入循環(huán) 如果添加成功就調(diào)用 addCount() 方法統(tǒng)計(jì) size,并且檢查是否需要擴(kuò)容。
helpTransfer():調(diào)用多個(gè)工作線程一起幫助進(jìn)行擴(kuò)容,這樣的效率就會(huì)更高。
計(jì)算 hash 值,定位到該 table 索引位置,如果是首結(jié)點(diǎn)符合就返回; 如果遇到擴(kuò)容時(shí),會(huì)調(diào)用標(biāo)記正在擴(kuò)容結(jié)點(diǎn) ForwardingNode.find()方法,查找該結(jié)點(diǎn),匹配就返回; 以上都不符合的話,就往下遍歷結(jié)點(diǎn),匹配就返回,否則最后就返回 null。
推薦閱讀:
【179期】這些最常用的Linux命令都不會(huì),你怎么敢去面試?
【178期】面試官:談?wù)勗谧鲰?xiàng)目過程中,你是是如何進(jìn)行SQL優(yōu)化的
【177期】拋開硬實(shí)力,如何寫簡歷才能幫你更快爭取到面試機(jī)會(huì)?
微信掃描二維碼,關(guān)注我的公眾號
朕已閱 
評論
圖片
表情

