日本黄色一级经典视频|伊人久久精品视频|亚洲黄色色周成人视频九九九|av免费网址黄色小短片|黄色Av无码亚洲成年人|亚洲1区2区3区无码|真人黄片免费观看|无码一级小说欧美日免费三级|日韩中文字幕91在线看|精品久久久无码中文字幕边打电话

當前位置:首頁 > > 架構師社區(qū)
[導讀]前言 HashMap應該算是Java后端工程師面試的必問題,因為其中的知識點太多,很適合用來考察面試者的Java基礎。 開場 面試官: 你先自我介紹一下吧! 安琪拉: 我是安琪拉,草叢三婊之一,最強中單(鐘馗不服)!哦,不對,串場了,我是**,目前在--公司做--系統(tǒng)

一個HashMap跟面試官扯了半個小時

前言

HashMap應該算是Java后端工程師面試的必問題,因為其中的知識點太多,很適合用來考察面試者的Java基礎。

開場

面試官: 你先自我介紹一下吧!

安琪拉: 我是安琪拉,草叢三婊之一,最強中單(鐘馗不服)!哦,不對,串場了,我是**,目前在--公司做--系統(tǒng)開發(fā)。

面試官: 看你簡歷上寫熟悉Java集合,HashMap用過的吧?

安琪拉: 用過的。(還是熟悉的味道)

面試官: 那你跟我講講HashMap的內部數(shù)據(jù)結構?

安琪拉: 目前我用的是JDK1.8版本的,內部使用數(shù)組 + 鏈表 / 紅黑樹;

安琪拉: 方便我給您畫個數(shù)據(jù)結構圖吧:

一個HashMap跟面試官扯了半個小時

面試官: 那你清楚HashMap的數(shù)據(jù)插入原理嗎?

安琪拉: 呃[做沉思狀]。我覺得還是應該畫個圖比較清楚,如下:

一個HashMap跟面試官扯了半個小時

  1. 判斷數(shù)組是否為空,為空進行初始化;
  2. 不為空,計算 k 的 hash 值,通過  (n - 1) & hash計算應當存放在數(shù)組中的下標  index ;
  3. 查看 table[index] 是否存在數(shù)據(jù),沒有數(shù)據(jù)就構造一個Node節(jié)點存放在 table[index] 中;
  4. 存在數(shù)據(jù),說明發(fā)生了hash沖突, 繼續(xù)判斷key是否相等,相等,用新的value替換原數(shù)據(jù)(onlyIfAbsent為false);
  5. 如果不相等,判斷當前節(jié)點類型是不是樹型節(jié)點,如果是樹型節(jié)點,創(chuàng)建樹型節(jié)點插入紅黑樹中;
  6. 如果不是樹型節(jié)點,創(chuàng)建普通Node加入鏈表中;判斷鏈表長度是否大于 8, 大于的話鏈表轉換為紅黑樹;
  7. 插入完成之后判斷當前節(jié)點數(shù)是否大于閾值,如果大于開始擴容為原數(shù)組的二倍。

面試官: 剛才你提到HashMap的初始化,那HashMap怎么設定初始容量大小的嗎?

安琪拉: [這也算問題??] 一般如果new HashMap() 不傳值,默認大小是16,負載因子是0.75, 如果自己傳入初始大小k,初始化大小為 大于k的 2的整數(shù)次方,例如如果傳10,大小為16。(補充說明:實現(xiàn)代碼如下)

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 < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}

補充說明:下圖是詳細過程,算法就是讓初始二進制分別右移1,2,4,8,16位,與自己異或,把高位第一個為1的數(shù)通過不斷右移,把高位為1的后面全變?yōu)?,111111 + 1 = 1000000  =  (符合大于50并且是2的整數(shù)次冪 )

一個HashMap跟面試官扯了半個小時

面試官:  你提到hash函數(shù),你知道HashMap的哈希函數(shù)怎么設計的嗎?

安琪拉:  [問的還挺細] hash函數(shù)是先拿到通過key 的hashcode,是32位的int值,然后讓hashcode的高16位和低16位進行異或操作。

一個HashMap跟面試官扯了半個小時

面試官:  那你知道為什么這么設計嗎?

安琪拉:  [這也要問],這個也叫擾動函數(shù),這么設計有二點原因:

  1. 一定要盡可能降低hash碰撞,越分散越好;
  2. 算法一定要盡可能高效,因為這是高頻操作, 因此采用位運算;

面試官:  為什么采用hashcode的高16位和低16位異或能降低hash碰撞?hash函數(shù)能不能直接用key的hashcode?

[這問題有點刁鉆], 安琪拉差點原地了,恨不得出biubiubiu 二一三連招。

安琪拉:  因為 key.hashCode() 函數(shù)調用的是key鍵值類型自帶的哈希函數(shù),返回int型散列值。int值范圍為**-2147483648~2147483647**,前后加起來大概40億的映射空間。只要哈希函數(shù)映射得比較均勻松散,一般應用是很難出現(xiàn)碰撞的。但問題是一個40億長度的數(shù)組,內存是放不下的。你想,如果HashMap數(shù)組的初始大小才16,用之前需要對數(shù)組的長度取模運算,得到的余數(shù)才能用來訪問數(shù)組下標。

源碼中模運算就是把散列值和數(shù)組長度-1做一個"與"操作,位運算比%運算要快。

bucketIndex = indexFor(hash, table.length);
static int indexFor(int h, int length) { return h & (length-1);}

順便說一下,這也正好解釋了為什么HashMap的數(shù)組長度要取2的整數(shù)冪。因為這樣(數(shù)組長度-1)正好相當于一個“低位掩碼”?!芭c”操作的結果就是散列值的高位全部歸零,只保留低位值,用來做數(shù)組下標訪問。以初始長度16為例,16-1=15。2進制表示是00000000 00000000 00001111。和某散列值做“與”操作如下,結果就是截取了最低的四位值。


     
 10100101 11000100 00100101& 00000000 00000000 00001111---------------------------------- 00000000 00000000 00000101 //高位全部歸零,只保留末四位

但這時候問題就來了,這樣就算我的散列值分布再松散,要是只取最后幾位的話,碰撞也會很嚴重。更要命的是如果散列本身做得不好,分布上成等差數(shù)列的漏洞,如果正好讓最后幾個低位呈現(xiàn)規(guī)律性重復,就無比蛋疼。

這時候 hash 函數(shù)(“擾動函數(shù)”)的價值就體現(xiàn)出來了,說到這里大家應該猜出來了。看下面這個圖,

一個HashMap跟面試官扯了半個小時

右位移16位,正好是32bit的一半,自己的高半?yún)^(qū)和低半?yún)^(qū)做異或,就是為了混合原始哈希碼的高位和低位,以此來加大低位的隨機性。而且混合后的低位摻雜了高位的部分特征,這樣高位的信息也被變相保留下來。

最后我們來看一下Peter Lawley的一篇專欄文章《An introduction to optimising a hashing strategy》里的的一個實驗:他隨機選取了352個字符串,在他們散列值完全沒有沖突的前提下,對它們做低位掩碼,取數(shù)組下標。

一個HashMap跟面試官扯了半個小時

結果顯示,當HashMap數(shù)組長度為512的時候( ),也就是用掩碼取低9位的時候,在沒有擾動函數(shù)的情況下,發(fā)生了103次碰撞,接近30%。而在使用了擾動函數(shù)之后只有92次碰撞。碰撞減少了將近10%??磥頂_動函數(shù)確實還是有功效的。

另外Java1.8相比1.7做了調整,1.7做了四次移位和四次異或,但明顯Java 8覺得擾動做一次就夠了,做4次的話,多了可能邊際效用也不大,所謂為了效率考慮就改成一次了。

下面是1.7的hash代碼:

static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4);}

面試官:  看來做過功課,有點料?。∈遣皇峭低悼戳?strong>安琪拉的博客公眾號, 你剛剛說到1.8對hash函數(shù)做了優(yōu)化,1.8還有別的優(yōu)化嗎?

安琪拉: 1.8還有三點主要的優(yōu)化:

  1. 數(shù)組+鏈表改成了數(shù)組+鏈表或紅黑樹;
  2. 鏈表的插入方式從頭插法改成了尾插法,簡單說就是插入時,如果數(shù)組位置上已經(jīng)有元素,1.7將新元素放到數(shù)組中,原始節(jié)點作為新節(jié)點的后繼節(jié)點,1.8遍歷鏈表,將元素放置到鏈表的最后;
  3. 擴容的時候1.7需要對原數(shù)組中的元素進行重新hash定位在新數(shù)組的位置,1.8采用更簡單的判斷邏輯,位置不變或索引+舊容量大??;
  4. 在插入時,1.7先判斷是否需要擴容,再插入,1.8先進行插入,插入完成再判斷是否需要擴容;

面試官:  你分別跟我講講為什么要做這幾點優(yōu)化;

安琪拉:  【咳咳,果然是連環(huán)炮】

  1. 防止發(fā)生hash沖突,鏈表長度過長,將時間復雜度由O(n)降為O(logn);

  2. 因為1.7頭插法擴容時,頭插法會使鏈表發(fā)生反轉,多線程環(huán)境下會產(chǎn)生環(huán);

    A線程在插入節(jié)點B,B線程也在插入,遇到容量不夠開始擴容,重新hash,放置元素,采用頭插法,后遍歷到的B節(jié)點放入了頭部,這樣形成了環(huán),如下圖所示:

一個HashMap跟面試官扯了半個小時

1.7的擴容調用transfer代碼,如下所示:

void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; //A線程如果執(zhí)行到這一行掛起,B線程開始進行擴容 newTable[i] = e; e = next; } }}
  1. 擴容的時候為什么1.8 不用重新hash就可以直接定位原節(jié)點在新數(shù)據(jù)的位置呢?

    這是由于擴容是擴大為原數(shù)組大小的2倍,用于計算數(shù)組位置的掩碼僅僅只是高位多了一個1,舉個例子:

   擴容前長度為16,用于計算 (n-1) & hash 的二進制n - 1為0000 1111,  

     擴容后為32后的二進制就高位多了1,============>為0001 1111。

  1. 因為是& 運算,1和任何數(shù) & 都是它本身,那就分二種情況,如下圖:原數(shù)據(jù)hashcode高位第4位為0和高位為1的情況;

    第四位高位為0,重新hash數(shù)值不變,第四位為1,重新hash數(shù)值比原來大16(舊數(shù)組的容量)

    一個HashMap跟面試官扯了半個小時

面試官:  那HashMap是線程安全的嗎?

安琪拉:  不是,在多線程環(huán)境下,1.7 會產(chǎn)生死循環(huán)、數(shù)據(jù)丟失、數(shù)據(jù)覆蓋的問題,1.8 中會有數(shù)據(jù)覆蓋的問題。

以1.8為例,當A線程執(zhí)行到下面代碼第6行判斷index位置為空后正好掛起,B線程開始執(zhí)行第行,往index位置的寫入節(jié)點數(shù)據(jù),這時A線程恢復現(xiàn)場,執(zhí)行賦值操作,就把A線程的數(shù)據(jù)給覆蓋了;

還有第38++size這個地方也會造成多線程同時擴容等問題。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) //多線程執(zhí)行到這里 tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) // 多個線程走到這,可能重復resize() resize(); afterNodeInsertion(evict); return null;}

面試官:  那你平常怎么解決這個線程不安全的問題?

安琪拉:  Java中有HashTable、Collections.synchronizedMap、以及ConcurrentHashMap可以實現(xiàn)線程安全的Map。

  1. HashTable是直接在操作方法上加synchronized關鍵字,鎖住整個數(shù)組,粒度比較大;

  2. Collections.synchronizedMap是使用Collections集合工具的內部類,通過傳入Map封裝出一個SynchronizedMap對象,內部定義了一個對象鎖,方法內通過對象鎖實現(xiàn);

  3. ConcurrentHashMap使用分段鎖,降低了鎖粒度,讓并發(fā)度大大提高。

面試官:  那你知道ConcurrentHashMap的分段鎖的實現(xiàn)原理嗎?

安琪拉:  【天啦擼! 俄羅斯套娃,一個套一個】ConcurrentHashMap成員變量使用volatile 修飾,免除了指令重排序,同時保證內存可見性,另外使用CAS操作和synchronized結合實現(xiàn)賦值操作,多線程操作只會鎖住當前操作索引的節(jié)點。

如下圖,線程A鎖住A節(jié)點所在鏈表,線程B鎖住B節(jié)點所在鏈表,操作互不干涉。

一個HashMap跟面試官扯了半個小時

面試官:  你前面提到鏈表轉紅黑樹是鏈表長度達到閾值,這個閾值是多少?

安琪拉:  閾值是8,紅黑樹轉鏈表閾值為6

面試官:  為什么是8,不是16,32甚至是7 ?又為什么紅黑樹轉鏈表的閾值是6,不是8了呢?

安琪拉: 【你去問作者?。√炖矓],biubiubiu 真想213連招】

因為作者就這么設計的,哦,不對,因為經(jīng)過計算,在hash函數(shù)設計合理的情況下,發(fā)生hash碰撞8次的幾率為百萬分之6,概率說話。。因為8夠用了,至于為什么轉回來是6,因為如果hash碰撞次數(shù)在8附近徘徊,會一直發(fā)生鏈表和紅黑樹的轉化,為了預防這種情況的發(fā)生。

面試官:  HashMap內部節(jié)點是有序的嗎?

安琪拉:  是無序的,根據(jù)hash值隨機插入

面試官:  那有沒有有序的Map?

安琪拉:  LinkedHashMap 和 TreeMap

面試官:  跟我講講LinkedHashMap怎么實現(xiàn)有序的?

安琪拉:  LinkedHashMap內部維護了一個單鏈表,有頭尾節(jié)點,同時LinkedHashMap節(jié)點Entry內部除了繼承HashMap的Node屬性,還有before 和 after用于標識前置節(jié)點和后置節(jié)點??梢詫崿F(xiàn)按插入的順序或訪問順序排序。

/** * The head (eldest) of the doubly linked list.*/transient LinkedHashMap.Entry<K,V> head;
/** * The tail (youngest) of the doubly linked list.*/transient LinkedHashMap.Entry<K,V> tail;//鏈接新加入的p節(jié)點到鏈表后端private void linkNodeLast(LinkedHashMap.Entry<K,V> p) { LinkedHashMap.Entry<K,V> last = tail; tail = p; if (last == null) head = p; else { p.before = last; last.after = p; }}//LinkedHashMap的節(jié)點類static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); }}

示例代碼:

public static void main(String[] args) { Map<String, String> map = new LinkedHashMap<String, String>(); map.put("1", "安琪拉"); map.put("2", "的"); map.put("3", "博客");
for(Map.Entry<String,String> item: map.entrySet()){ System.out.println(item.getKey() + ":" + item.getValue()); }}//console輸出1:安琪拉2:的3:博客

面試官:  跟我講講TreeMap怎么實現(xiàn)有序的?

安琪拉:TreeMap是按照Key的自然順序或者Comprator的順序進行排序,內部是通過紅黑樹來實現(xiàn)。所以要么key所屬的類實現(xiàn)Comparable接口,或者自定義一個實現(xiàn)了Comparator接口的比較器,傳給TreeMap用戶key的比較。

面試官:  前面提到通過CAS 和 synchronized結合實現(xiàn)鎖粒度的降低,你能給我講講CAS 的實現(xiàn)以及synchronized的實現(xiàn)原理嗎?

安琪拉:  下一期咋們再約時間,OK?

面試官:  好吧,回去等通知吧!



參考資料

  1. An introduction to optimising a hashing strategy
  2. JDK 源碼中 HashMap 的 hash 方法原理是什么?
  3. 淡騰的楓-HashMap中的hash函數(shù)

特別推薦一個分享架構+算法的優(yōu)質內容,還沒關注的小伙伴,可以長按關注一下:

一個HashMap跟面試官扯了半個小時

長按訂閱更多精彩▼

一個HashMap跟面試官扯了半個小時

如有收獲,點個在看,誠摯感謝

免責聲明:本文內容由21ic獲得授權后發(fā)布,版權歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!

本站聲明: 本文章由作者或相關機構授權發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內容真實性等。需要轉載請聯(lián)系該專欄作者,如若文章內容侵犯您的權益,請及時聯(lián)系本站刪除。
換一批
延伸閱讀

LED驅動電源的輸入包括高壓工頻交流(即市電)、低壓直流、高壓直流、低壓高頻交流(如電子變壓器的輸出)等。

關鍵字: 驅動電源

在工業(yè)自動化蓬勃發(fā)展的當下,工業(yè)電機作為核心動力設備,其驅動電源的性能直接關系到整個系統(tǒng)的穩(wěn)定性和可靠性。其中,反電動勢抑制與過流保護是驅動電源設計中至關重要的兩個環(huán)節(jié),集成化方案的設計成為提升電機驅動性能的關鍵。

關鍵字: 工業(yè)電機 驅動電源

LED 驅動電源作為 LED 照明系統(tǒng)的 “心臟”,其穩(wěn)定性直接決定了整個照明設備的使用壽命。然而,在實際應用中,LED 驅動電源易損壞的問題卻十分常見,不僅增加了維護成本,還影響了用戶體驗。要解決這一問題,需從設計、生...

關鍵字: 驅動電源 照明系統(tǒng) 散熱

根據(jù)LED驅動電源的公式,電感內電流波動大小和電感值成反比,輸出紋波和輸出電容值成反比。所以加大電感值和輸出電容值可以減小紋波。

關鍵字: LED 設計 驅動電源

電動汽車(EV)作為新能源汽車的重要代表,正逐漸成為全球汽車產(chǎn)業(yè)的重要發(fā)展方向。電動汽車的核心技術之一是電機驅動控制系統(tǒng),而絕緣柵雙極型晶體管(IGBT)作為電機驅動系統(tǒng)中的關鍵元件,其性能直接影響到電動汽車的動力性能和...

關鍵字: 電動汽車 新能源 驅動電源

在現(xiàn)代城市建設中,街道及停車場照明作為基礎設施的重要組成部分,其質量和效率直接關系到城市的公共安全、居民生活質量和能源利用效率。隨著科技的進步,高亮度白光發(fā)光二極管(LED)因其獨特的優(yōu)勢逐漸取代傳統(tǒng)光源,成為大功率區(qū)域...

關鍵字: 發(fā)光二極管 驅動電源 LED

LED通用照明設計工程師會遇到許多挑戰(zhàn),如功率密度、功率因數(shù)校正(PFC)、空間受限和可靠性等。

關鍵字: LED 驅動電源 功率因數(shù)校正

在LED照明技術日益普及的今天,LED驅動電源的電磁干擾(EMI)問題成為了一個不可忽視的挑戰(zhàn)。電磁干擾不僅會影響LED燈具的正常工作,還可能對周圍電子設備造成不利影響,甚至引發(fā)系統(tǒng)故障。因此,采取有效的硬件措施來解決L...

關鍵字: LED照明技術 電磁干擾 驅動電源

開關電源具有效率高的特性,而且開關電源的變壓器體積比串聯(lián)穩(wěn)壓型電源的要小得多,電源電路比較整潔,整機重量也有所下降,所以,現(xiàn)在的LED驅動電源

關鍵字: LED 驅動電源 開關電源

LED驅動電源是把電源供應轉換為特定的電壓電流以驅動LED發(fā)光的電壓轉換器,通常情況下:LED驅動電源的輸入包括高壓工頻交流(即市電)、低壓直流、高壓直流、低壓高頻交流(如電子變壓器的輸出)等。

關鍵字: LED 隧道燈 驅動電源
關閉