Java HashMap和 ConcurrentHashMap 熱門面試題


目錄

  更多關於HashMap的知識點,請戳《HashMap知識點梳理、常見面試題和源碼分析》。

  HashMap的結構無疑是Java面試中出現頻率最高的一道題,此題是如此之常見,每個人都應該信手拈來,然而,能完整回答HashMap問題的人卻是寥寥無幾。

對於一位中高級java程序員而言,若對集合類的內部原理不了解,基本上面試都會被pass掉。故,下面從面試官的角度梳理了一份精選面試題,來聊聊一位候選者應該對HashMap了解到什么程度才算是合格。

  樓蘭胡楊希望大家不但研究過JDK中HashMap的源代碼,而且熟悉不同版本JDK中使用的優化機制。當然了,如果具有手動實現HashMap的能力就更優秀了。

在日常開發中使用過的java集合類有哪些

  一般應聘者都會回答ArrayList,LinkedList,HashMap,HashSet等等。如果連這幾個集合類都不知道,基本上可以pass了。

談一下HashMap的特性

  • HashMap初始化時使用懶加載機制,只初始化變量,未初始化數組,數組在首次添加元素時初始化。
  • 存儲鍵值對,實現快速存取。key值不可重復,若key值重復則覆蓋。
  • 鍵和值位置都可以是null,但是鍵位置只能是一個null。
  • 非同步,線程不安全。
  • 底層是hash表,不保證有序(比如插入的順序)。
  • 鏈表長度不小於8並且數組長度大於64,才將鏈表轉換成紅黑樹,變成紅黑樹的目的是提高搜索速度,高效查詢

HashMap 的數據結構是什么

  自Java 8 開始,哈希表結構(鏈表散列)由數組+單鏈表+紅黑樹實現,結合Node數組和單鏈表的優點。當鏈表長度不小於 8且數組長度不小於64時,鏈表轉換為紅黑樹;否則,擴容。數組是HashMap的主題,鏈表和紅黑樹主要是為了解決哈希沖突(拉鏈法解決沖突)。

單鏈表和紅黑樹相互轉換的條件是什么

  當單鏈表長度不小於8,並且桶的個數不小於64時,將單鏈表轉化為紅黑樹,以減少搜索時間。

  同樣,后續如果由於刪除或者其它原因調整了大小,當紅黑樹的節點數不大於 6時,又會轉換為鏈表。

  hashCode 均勻分布時,TreeNode 用到的機會很小。理想情況下bin 中節點的分布遵循泊松分布,一個 bin 中鏈表長度達到 8 的概率(0.00000006)不足千萬分之一,因此將轉換的閾值設為 8。

  通常如果 hash 算法正常的話,鏈表的長度也不會很長,那么紅黑樹也不會帶來明顯的查詢時間上的優勢,反而會增加空間負擔(TreeNode的大小大約是常規節點Node的兩倍)。所以通常情況下,並沒有必要轉為紅黑樹。

鏈表和紅黑樹相互轉換的閾值為什么是 8 和 6

  如果選擇6和8,中間有個差值7可以有效防止鏈表和紅黑樹頻繁轉換。如果一個 HashMap 不停地進行插入和刪除元素,鏈表的個數一直在 8 左右徘徊,這種情況會頻繁地進行紅黑樹和鏈表的相互轉換,效率很低。

  hashCode 均勻分布時,TreeNode 用到的機會很小。理想情況下bin 中節點的分布遵循泊松分布,一個 bin 中鏈表長度達到 8 的概率(0.00000006)不足千萬分之一,因此將轉換的閾值設為 8。

為什么要在數組長度不小於64之后,鏈表才會進化為紅黑樹

  如果在數組比較小時出現紅黑樹結構,反而會降低效率,而紅黑樹需要通過左旋、右旋和變色操作來保持平衡。同時數組長度小於64時,搜索時間相對要快些,總之是為了加快搜索速度,提高性能。

  Java 8 以前HashMap的實現是數組+鏈表,即使哈希函數取得再好,也很難達到元素百分百均勻分布。當HashMap中有大量的元素都存放在同一個桶中時,這個桶下有一條長長的鏈表,此時HashMap就相當於單鏈表,假如單鏈表有n個元素,遍歷的時間復雜度就從O(1)退化成O(n),完全失去了它的優勢。為了解決此種情況,Java 8中引入了紅黑樹(查找的時間復雜度為O(logn))。

HashMap 的容量如何確定

  容量就是HashMap中的數組大小,是由 capacity 這個參數確定的。默認是16,也可以構造時傳入,最大限制是1<<30;

  HashMap采用懶加載機制,也就是說在執行new HashMap()的時候,構造方法並沒有在構造HashMap實例的同時也初始化實例里的數組。那么什么時候才去初始化數組呢?答案是只有在第一次需要用到這個數組的時候才會去初始化它,就是在你往HashMap里面put元素的時候

loadFactor 是什么

  loadFactor 是裝載因子,主要目的是用來確認table 數組是否需要動態擴展,默認值是0.75,比如table 數組大小為 16,裝載因子為 0.75 時,threshold 就是12,當 table 的實際大小超過 12 時,table就需要動態擴容。

  空間換時間:如果希望加快Key查找的時間,還可以進一步降低加載因子,加大初始容量大小,以降低哈希沖突的概率。

  性能分析:空桶太多會浪費空間,如果使用的太滿則會嚴重影響操作的性能。

HashMap使用了哪些方法來有效解決哈希沖突

  1、使用鏈地址法(使用散列表)來鏈接擁有相同hash值的數據。

  2、使用2次擾動函數(hash函數)降低哈希沖突的概率,使得數據分布更均勻。

  3、引入紅黑樹進一步降低遍歷的時間復雜度。

為什么數組長度要保證為2的冪次方

  只有當數組長度為2的冪次方時,h&(length-1)才等價於h%length,即實現了key的定位,2的冪次方也可以減少哈希沖突次數,提高HashMap的查詢效率。

  如果 length 為 2 的次冪 則 length-1 轉化為二進制必定是 11111……的形式,在於 h 的二進制與操作效率會非常的快,而且空間不浪費;如果 length 不是 2 的次冪,比如 length 為 15,則 length - 1 為 14,對應的二進制為 1110,在於 h 與操作,最后一位都為 0 ,而 0001,0011,0101,1001,1011,0111,1101 這幾個位置永遠都不能存放元素了,空間浪費相當大,更糟的是這種情況中,數組可以使用的位置比數組長度小了很多,這意味着進一步增加了碰撞的幾率,減慢了查詢的效率!這樣就會造成空間的浪費。

為什么是兩次擾動

  加大哈希值低位的隨機性,使得分布更均勻,從而提高數組下標位置的隨機性和均勻性,最終減少哈希沖突。兩次就夠了,已經達到了高位和低位同時參與運算的目的。

數組擴容機制是什么

  擴容方法是resize()方法。

  • 默認是空數組,初始化的容量是16,即桶的個數默認為16;
  • 總元素個數超過容量✖️加載因子時,進行數組擴容;
  • 創建一個新的數組,其大小為舊數組大小的兩倍,並重新計算舊數組中結點的存儲位置。
  • 結點在新數組中的位置只有兩種,原下標位置或原下標+舊數組的大小。

  擴容帶來的危害:在數據量很大的情況下擴容將會帶來性能的損失,在性能要求很高的地方,這種損失可能很致命。擴容太頻繁就會導致內存抖動問題,增加瞬間的內存消耗和性能消耗,因此在創建HashMap的時候,如果能預知初始數據量的大小,在構造的時候可以設置初始容量,也可以設置擴容因子。

為什么要重新Hash,直接復制過去不好嗎

  哈希函數中,包括對數組長度取模,故長度擴大以后,哈希函數也隨之改變。

談一下hashMap中put是如何實現的

  1.基於key的hashcode值計算哈希值(與Key.hashCode的高16位做異或運算)
  2.如果散列表為空時,調用resize()初始化散列表
  3.如果沒有發生哈希碰撞(hash值相同),直接添加元素到散列表中去
  4.如果發生了哈希碰撞,進行三種判斷
    4.1:若key地址相同或者equals后內容相同,則替換舊值
    4.2:如果是紅黑樹結構,就調用樹的插入方法
    4.3:鏈表結構,循環遍歷直到鏈表中某個節點為空,尾插法進行插入,插入之后判斷鏈表是否樹化:當鏈表長度不小於 8且數組長度不小於64時,鏈表轉換為紅黑樹
  5.如果桶滿了,即元素個數大於閾值,則resize進行擴容

  hashCode 是定位的,用於確認數組下標;equals是定性的,比較兩者是否相等。

hashMap中get函數是如何實現的

  對key的hashCode進行哈希運算,然后借助與運算計算下標獲取bucket位置,如果在桶的首位上就可以找到就直接返回;否則,在樹或者鏈表中遍歷找。如果有hash沖突,則利用equals方法遍歷查找節點。

Java 7 與 Java 8 中,HashMap的區別

  1.數據結構不同 Java 7采用Entry數組+單鏈表的數據結構,而java 8 采用Node數組+單鏈表+紅黑樹,把時間復雜度從O(n)變成O(logN),提高了效率。這是Java 7與Java 8中HashMap實現的最大區別。

  1.hash沖突解決方案不同 發生hash沖突時,在Java 7中采用頭插法,新元素插入到鏈表頭中,即新元素總是添加到數組中,舊元素移動到鏈表中。 Java 8會優先判斷該節點的數據結構式是紅黑樹還是鏈表,如果是紅黑樹,則在紅黑樹中插入數據;如果是鏈表,則采用尾插法,將數據插入到鏈表的尾部,然后判斷是否需要轉成紅黑樹。

  鏈表插入元素時,Java 7用的是頭插法,而Java 8及之后使用的都是尾插法,那么他們為什么要這樣做呢?因為JDK1.7是用單鏈表進行的縱向延伸,當采用頭插法時會容易出現逆序且環形鏈表死循環問題。但是在JDK1.8之后是因為加入了紅黑樹使用尾插法,能夠避免出現逆序且鏈表死循環的問題。
頭插法優點: resize后transfer數據時不需要遍歷鏈表到尾部再插入;最近put的可能等下就被get,頭插遍歷到鏈表頭就匹配到了。

  2.擴容機制不同 Java 7:在擴容resize()過程中,采用單鏈表的頭插入方式,在線程下將舊數組上的數據 轉移到 新數組上時,容易出現死循環。此時若(多線程)並發執行 put()操作,一旦出現擴容情況,則容易出現環形鏈表,從而在獲取數據、遍歷鏈表時形成死循環(Infinite Loop),即死鎖的狀態。

  Java 8:由於 Java 8 轉移數據操作 = 按舊鏈表的正序遍歷鏈表、在新鏈表的尾部依次插入,所以不會出現鏈表 逆序、倒置的情況,故不容易出現環形鏈表的情況。

  3. 擴容后存放位置不同 java 7 受rehash影響,java 8 調整后是原位置 or 原位置+舊容量

  使用HashMap時,樓蘭胡楊的一些經驗之談:

  1. 使用時設置初始值,避免多次擴容的性能消耗。
  2. 使用自定義對象作為key時,需要重寫hashCode和equals方法。
    3.多線程下,使用CurrentHashMap代替HashMap。

HashMap的key一般使用什么數據類型

  String、Integer等包裝類的特性可以保證哈希值的不可更改性和計算准確性,可以有效地減少哈希碰撞的概率。

  都是final類型,即不可變類,作為不可變類天生是線程安全的。這些包裝類已重寫了equals()和hashCode()等方法,保證key的不可更改性,不會存在多次獲取哈希值時哈希值卻不相同的情況。

如果讓自己的創建的類作為HashMap的key,應該怎么實現

  重寫hashCode()和equals()方法。

  重寫hashCode()是因為需要計算存儲數據的存儲位置,需要注意不要試圖從散列碼計算中排除掉一個對象的關鍵部分來提高性能,這樣雖然能更快但可能會導致更多的Hash碰撞。重寫equals()方法,需要遵守自反性、對稱性、傳遞性、一致性以及對於任何非null的引用值x,x.equals(null)必須返回false的這幾個特性,目的是為了保證key在哈希表中的唯一性。

HashMap為什么由Java 7 的頭插法改為Java 8的尾插法

  當HashMap要在鏈表里插入新的元素時,在Java 8之前是將元素插入到鏈表頭部,自Java 8開始插入到鏈表尾部(Java 8用Node對象替代了Entry對象)。Java 7 插入鏈表頭部,是考慮到新插入的數據,更可能作為熱點數據被使用,放在頭部可以減少查找時間。Java 8改為插入鏈表尾部是為了防止環化。因為resize的賦值方式,也就是使用了單鏈表的頭插入方式,同一位置上新元素總會被放在鏈表的頭部位置,在舊數組中同一條Entry鏈上的元素,通過重新計算索引位置后,有可能被放到了新數組的不同位置上。

HashMap與Hashtable的區別是什么

  HashMap與Hashtable的區別見如下表格:

不同點 HashMap Hashtable
數據結構 數組 + 單鏈表 + 紅黑樹 數組+鏈表
繼承的類 AbstractMap Dictonary,但二者都實現了map接口
線程安全
性能搞定 低,因為需要保證線程安全
默認初始化容量 16
擴容方式 數組大小×2 數組大小×2+1
底層數組容量 一定為2的整數冪次 不要求
hash值算法 (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16) key.hashCode()
數組下標計算方法 (n-1) & hash hash&0x7FFFFFFF%length
key-value是否允許null
是否提供contains方法 有containsvalue和containsKey方法 有contains方法方法
遍歷方式 Iterator Iterator 和 Enumeration

  看完之后,是不是可以和面試官PK三分鍾了?更多內容請戳《面試題:HashMap和Hashtable的區別和聯系》。

HashMap為什么線程不安全

  多線程下擴容死循環。JDK1.7中的HashMap使用頭插法插入元素,在多線程的環境下,擴容的時候有可能導致環形鏈表的出現,形成死循環。因此JDK1.8使用尾插法插入元素,在擴容時會保持鏈表元素原本的順序,不會出現環形鏈表的問題。

  多線程的put可能導致元素的丟失。多線程同時執行put操作,如果計算出來的索引位置是相同的,那會造成前一個key被后一個key覆蓋,從而導致元素的丟失。此問題在JDK1.7和JDK1.8中都存在。

  put和get並發時,可能導致get為null。線程1執行put時,因為元素個數超出threshold而導致rehash,線程2此時執行get,有可能導致這個問題,此問題在JDK1.7和JDK1.8中都存在。

jdk8中對HashMap做了哪些改變

  • 在java 8中,引入了紅黑樹,鏈表可以轉換為紅黑樹。
  • 發生hash碰撞時,java 7 會在鏈表的頭部插入,而java 8會在鏈表的尾部插入。
  • 在java 8中,Entry被Node替代(換了一個馬甲。

拉鏈法導致的鏈表過深問題為什么不用二叉查找樹代替,而選擇紅黑樹?為什么不一直使用紅黑樹

  之所以選擇紅黑樹是為了解決二叉查找樹的缺陷,二叉查找樹在特殊情況下會變成一條線性結構(這就跟原來使用鏈表結構一樣了,造成很深的問題),遍歷查找會非常慢。推薦:面試問紅黑樹,我臉都綠了。而紅黑樹在插入新數據后可能需要通過左旋、右旋和變色這些操作來保持平衡,引入紅黑樹就是為了查找數據快,解決鏈表查詢深度的問題。我們知道紅黑樹屬於平衡二叉樹,為了保持“平衡”是需要付出代價的,但是該代價所損耗的資源比遍歷線性鏈表少很多,所以當鏈表長度不小於8且數組長度大於64的時候,會使用紅黑樹,如果鏈表長度很短的話,根本引入紅黑樹反而會降低效率。

HashMap為什么引入紅黑樹

  Java8以前 HashMap 的實現是 數組+鏈表,即使哈希函數取得再好,也很難達到元素百分百均勻分布。當 HashMap 中有大量的元素發生哈希碰撞時,這些元素都被存放到同一個桶中,從而形成一條長長的鏈表,此時 HashMap 就相當於一個單鏈表,遍歷的時間復雜度會退化到O(n),完全失去了它的優勢。針對這種情況,JAVA 8 中引入了紅黑樹來優化這個問題,紅黑樹的好處就是它的自平衡性,n個節點的樹的查找時間復雜度只有 O(log n)。

如果兩個鍵的哈希值相同,如何獲取值對象

  哈希值相同,通過equals比較內容獲取值對象。

說說你對紅黑樹的見解

  • 每個節點非紅即黑。
  • 根節點總是黑色的。
  • 如果節點是紅色的,則它的子節點必須是黑色的(反之不一定);
  • 每個葉子節點都是黑色的空節點(NIL節點);
  • 從根節點到葉節點或空子節點的每條路徑,必須包含相同數目的黑色節點(即相同的黑色高度)。

為什么是16?為什么必須是2的冪?如果輸入值不是2的冪比如10會怎么樣

  為什么槽位數必須使用2^n

①為了使得數據均勻分布,減少哈希碰撞。因為確定數組位置使用的位運算,若數據不是2的次冪則會增加哈希碰撞的次數和浪費數組空間。(PS:其實若不考慮效率,求余也可以就不用位運算了也不用長度必需為2的冪次)。

  輸入數據若不是2的冪,HashMap通過一通位移運算和或運算得到的肯定是2的冪次數,並且是離那個數最近的數字。

②等價於length取模

  當length總是2的n次方時,h& (length-1)運算等價於對length取模,也就是h%length,但是&比%具有更高的效率。位運算的運算效率高於算術運算,原因是算術運算還是會被轉化為位運算。最終目的還是為了讓哈希后的結果更均勻的分部,減少哈希碰撞,提升hashmap的運行效率。

為什么String, Interger這樣的wrapper類適合作為鍵

  String, Interger這樣的wrapper類作為HashMap的鍵是再適合不過了,而且String最為常用。因為String是不可變的,也是final的,而且已經重寫了equals()和hashCode()方法了。其它的wrapper類也有這個特點。不可變性是必要的,因為為了要計算hashCode(),就要防止鍵值改變,如果鍵值在放入時和獲取時返回不同的hashcode的話,那么就不能從HashMap中找到你想要的對象。不可變性還有其它的優點如線程安全。如果你可以僅僅通過將某個field聲明成final就能保證hashCode是不變的,那么請這么做吧。因為獲取對象的時候要用到equals()和hashCode()方法,那么鍵對象正確的重寫這兩個方法是非常重要的。如果兩個不相等的對象返回不同的hashcode的話,那么碰撞的幾率就會小些,這樣就能提高HashMap的性能。

如果不重寫作為可以的Bean的hashCode()方法,是否會對性能帶來影響

  這個問題非常好,仁者見仁智者見智。按照我掌握的知識來說,如果一個哈希方法寫得不好,直接的影響是,在向HashMap中添加元素的時候會更頻繁地造成哈希沖突,因此最終增加了耗時。但是自從Java 8開始,這種影響不再像前幾個版本那樣顯著了,因為當哈希沖突的發生超出了一定的限度之后,鏈表將會被替換成紅黑樹,這時你仍可以得到O(logN)的開銷,優於鏈表類的O(n)。

談一下當兩個對象的哈希值相等時會怎么樣

  會產生哈希碰撞,若key值相同則替換舊值,不然采用尾插法鏈接到鏈表后面,鏈表長度超過閾值8且桶的個數大於64就轉為紅黑樹存儲。

請解釋一下HashMap的參數loadFactor,它的作用是什么

  loadFactor是負載因子,表示HashMap的擁擠程度,影響hash操作到同一個數組位置的概率。默認loadFactor等於0.75,當HashMap里面容納的元素已經達到HashMap數組長度的75%時,表示HashMap太擠了,需要擴容,在HashMap的構造器中可以定制loadFactor。

Java 8 HashMap 擴容之后,舊元素存放位置是什么

  HashMap 在擴容的時候會創建一個新的 Node<K,V>[],用於存放擴容之后的值,並將舊的Node數組(其大小記作n)置空;至於舊值移動到新的節點的時候,存放於哪個節點是根據 (e.hash & oldCap) == 0 來判斷的:
① 等於0時,則將其索引位置h不變;
② 不等於0時,則將該節點在新數組的索引位置等於原索引位置加上舊數組長度。

你了解重新調整HashMap大小存在什么問題嗎

  當hashMap因為擴容而調整hashMap的大小時,會導致之前計算出來的索引下標無效,所以所有的節點都需要重新進行哈希運算,結果就是帶來時間上的浪費。故建議盡量避免hashMap調整大小,所以我們使用hashMap的時候要給它設置一個初始容量,此值要大於hashMap中存放的節點個數。

你知道哈希函數的實現嗎?為什么要這樣實現?還有哪些hash函數的實現方式

  Java 8 中使用了異或運算,通過 key的hashCode() 的高 16 位異或低 16 位實現:(h = k.hashCode()) ^ (h >>> 16),此實現方案主要是從速度、功效和質量三個角度來考量的,用於減少系統的開銷,也可以避免因為高位沒有參與下標的計算而造成哈希碰撞。

  其它實現方式還有平方取中法,除留余數法,偽隨機數法等。

  如果面試者的技術面比較寬,或者算法基礎以及數論基礎比較好,這個問題才可以做很好的回答。首先,hashCode()不要求唯一但是要盡可能的均勻分布,而且算法效率要盡可能的快

  如果都結束了,不要忘了再問一句你知道hash攻擊嗎?有避免手段嗎?就看面試者對各個jdk版本對HashMap的優化是否了解了。這就引出了另一個數據結構紅黑樹了。可以根據崗位需要繼續考察rb-tree,b-tree,lsm-tree等常用數據結構以及典型應用場景。

哈希函數為什么要用異或運算符

  保證了對象的 hashCode 的 32 位值只要有一位發生改變,返回的 hash值就會改變。盡可能的減少哈希碰撞。

能否讓HashMap實現線程安全

  HashMap可以通過下面的語句進行同步:Collections.synchronizeMap(hashMap)。 synchronizedMap()方法返回一個SynchronizedMap類的對象,而在SynchronizedMap類中使用了synchronized來保證對Map的操作是線程安全的,故效率其實也不高。

如果多個線程操作同一個HashMap對象會產生哪些非正常現象?

  HashMap在多線程環境下操作可能會導致程序死循環。

  其實這已經開始考察候選人對並發知識的掌握情況了。HashMap在resize的時候,如果多個線程並發操作如何導致死鎖的。面試者不一定知道,但是可以讓面試者分析。畢竟很多類庫在並發場景中不恰當使用HashMap導致過生產問題。

Java 中的另一個線程安全的、與 HashMap 極其類似的類是什么?同樣是線程安全,它與 Hashtable 在線程同步上有什么不同

  ConcurrentHashMap 類(是 Java並發包 java.util.concurrent 中提供的一個線程安全且高效的 HashMap 實現)。Hashtable 是使用 synchronized 關鍵字加鎖的原理(就是對對象加鎖);而針對ConcurrentHashMap,在 JDK 7 中采用分段鎖的方式,而JDK 8 中直接采用了CAS(無鎖算法)+ synchronized。

Get方法的流程是怎樣的

  先調用Key的hashcode方法拿到對象的hash值,然后用hash值對第一維數組的長度進行取模,得到數組的下標。這個數組下標所在的元素就是第二維鏈表的表頭。然后遍歷這個鏈表,使用Key的equals同鏈表元素進行比較,匹配成功即返回鏈表元素里存放的值。

Get方法的時間復雜度是多少

  答:是O(1)。很多人在回答這道題時腦細胞會出現短路現象,開始懷疑人生。明明是O(1)啊,平時都記得牢牢的,又在質疑由於Get方法的流程里需要遍歷鏈表,難道遍歷的時間復雜度不是O(n)么?

假如HashMap里的元素有100w個,請問鏈表的長度大概是多少

  鏈表的長度很短,相比總元素的個數可以忽略不計。這個時候小伙伴們的眼睛通常會開始發光,很童貞。作為面試官是很喜歡看到這種眼神的。我使用反射統計過HashMap里面鏈表的長度,在HashMap里放了100w個隨機字符串鍵值對,發現鏈表的長度幾乎從來沒有超過7這個數字,當我增大loadFactor的時候,才會偶爾冒出幾個長度為8的鏈表來。

請說明一下HashMap擴容的過程

  擴容需要重新分配一個新數組,新數組是老數組的2倍長,然后遍歷整個老結構,把所有的元素挨個重新hash分配到新結構中去。

  這個rehash的過程是很耗時的,特別是HashMap很大的時候,會導致程序卡頓,而2倍內存的關系還會導致內存瞬間溢出,實際上是3倍內存,因為老結構的內存在rehash結束之前還不能立即回收。那為什么不能在HashMap比較大的時候擴容擴少一點呢,關於這個問題我也沒有非常滿意的答案,我只知道hash的取模操作使用的是按位操作,按位操作需要限制數組的長度必須是2的指數。另外就是Java堆內存底層用的是TcMalloc這類library,它們在內存管理的分配單位就是以2的指數的單位,2倍內存的遞增有助於減少內存碎片,減少內存管理的負擔。

你了解Redis么,你知道Redis里面的字典是如何擴容的嗎

好,如果這道題你也回答正確了,恭喜你,毫無無疑,你是一位很有錢途的高級程序員。

HashMap & ConcurrentHashMap 的區別

  除了加鎖,原理上無太大區別。另外,HashMap 的鍵值對允許有null,但是ConCurrentHashMap 都不允許。

HashMap、LinkedHashMap和TreeMap 有什么區別

  LinkedHashMap 保存了記錄的插入順序,在用 Iterator 遍歷時,先取到的記錄肯定是先插入的;遍歷比 HashMap 慢;TreeMap 實現 SortMap 接口,能夠把它保存的記錄根據鍵排序(默認按鍵值升序排序,也可以指定排序的比較器)。

為什么 ConcurrentHashMap 比 Hashtable 效率高

  Hashtable 使用一把鎖(鎖住整個鏈表結構)處理並發問題,多個線程競爭一把鎖容易導致阻塞;而ConcurrentHashMap降低了鎖粒度。ConcurrentHashMap在JDK 7 中使用分段鎖(ReentrantLock + Segment + HashEntry),相當於把一個 HashMap 分成多個段,每段分配一把鎖,這樣支持多線程訪問。鎖粒度:基於 Segment,包含多個 HashEntry。在JDK 8 中,使用 CAS + synchronized + Node + 紅黑樹,鎖粒度為Node(首結點)(實現 Map.Entry)。

ConcurrentHashMap 在 Java 8 中,為什么要使用內置鎖 synchronized 來代替重入鎖 ReentrantLock

  ①、粒度降低了。

  每擴容一次,ConcurrentHashMap的並發度就增加一倍。

  ②、獲得JVM的支持。

  在大量的數據操作下,對於 JVM 的內存壓力,基於 API 的 可重入鎖 ReentrantLock 會開銷更多的內存,而且后續的性能優化空間更小。而且JVM 開發團隊沒有放棄 synchronized,JVM能夠在運行時做出更大的優化空間更大:鎖粗化、鎖消除、鎖自旋等等,這就使得synchronized能夠隨着JDK版本的升級而重構代碼的前提下獲得性能上的提升。

  ③、減少內存開銷

  假如使用可重入鎖獲得同步支持,那么每個節點都需要通過繼承AQS來獲得同步支持。但並不是每個節點都需要獲得同步支持的,只有鏈表的頭結點(紅黑樹的根節點)需要同步,這無疑造成了巨大的內存開銷。

ConcurrentHashMap 的並發度是什么

  程序運行時能夠同時更新 ConccurentHashMap 且不產生鎖競爭的最大線程數。默認為 16,且可以在構造函數中設置。當用戶設置並發度時,ConcurrentHashMap 會使用大於等於該值的最小2次冪指數作為實際並發度(假如用戶設置並發度為17,實際並發度則為32)。

ConcurrentHashMap 加鎖機制

  它加鎖的場景分為兩種:

  1、沒有發生hash沖突的時候,添加元素的位置在數組中是空的,使用CAS的方式來加入元素,這里加鎖的粒度是數組中的元素。

  2、如果出現了hash沖突,添加的元素的位置在數組中已經有了值,那么又存在三種情況。
    (1)key相同,則用新的元素覆蓋舊的元素。
    (2)如果數組中的元素是鏈表的形式,那么將新的元素掛載在鏈表尾部。
    (3)如果數組中的元素是紅黑樹的形式,那么將新的元素加入到紅黑樹。

  第二種場景使用的是synchronized加鎖,鎖住的對象就是鏈表頭節點(紅黑樹的根節點),加鎖的粒度和第一種情況相同。

  結論:ConcurrentHashMap分段加鎖機制其實鎖住的就是數組中的元素,當操作數組中不同的元素時,是不會產生競爭的。

ConcurrentHashMap 存儲對象的過程

1> 如果沒有初始化,就調用 initTable() 方法來進行初始化;
2> 如果沒有哈希沖突就直接 CAS 無鎖插入;
3> 如果需要擴容,就先進行擴容;
4> 如果存在哈希沖突,就加鎖來保證線程安全,兩種情況:一種是鏈表形式就直接遍歷到尾端插入,一種是紅黑樹就按照紅黑樹結構插入;
5> 如果該鏈表長度大於閥值 8(且數組中元素數量大於64),就要先轉換成紅黑樹的結構。

ConcurrentHashMap get操作需要加鎖嗎?線程安全嗎

  get 方法不需要加鎖。因為 Node 的元素 value 和指針 next 是用 volatile 修飾的,在多線程環境下線程A修改節點的 value 或者新增節點的時候是對線程B可見的。這也是它比其它並發集合比如 Hashtable、用 Collections.synchronizedMap()包裝的 HashMap 效率高的原因之一。

如何保證 HashMap 總是使⽤ 2 的冪次作為桶的⼤⼩

  /**
     \*  Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

HashSet 與 HashMap 的區別

  HashSet與HashMap的擴容機制一樣,區別見如下表格:

不同點 HashMap HashSet
實現的接口 Map接口 Set接口
存儲方式 存儲鍵值對 存儲對象
存儲方法 使用put函數 使用add函數
性能高低 快,因為使用唯一的鍵來獲取對象
默認初始化容量 16
哈希值 使用鍵對象計算哈希值 使用成員對象計算哈希值

結束語

  眼界不凡,前途無量。攻城獅們,加油吧!樓蘭胡楊祝君平步青雲。

Reference


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM