哈希表和 Java 的前世今生(下),掌握HashMap看這一篇就夠了!!!

哈希表和 Java 的前世今生(上),掌握HashMap看這一篇就夠了!!! 中,我們講解了哈希表的原理以及JDK7 HashMap的源碼及JDK7中HashMap的注意點
哈希表和 Java 的前世今生(中),掌握HashMap看這一篇就夠了!!!中,我們講解了JDK8 HashMap的源碼,以及其與JDK7 HashMap的區別。

五、簡單來看一下 Hashtable

在這裏插入圖片描述
HashMap 類大致相當於 Hashtable,只是它不同步並且允許空值。

5.1 問題 6:Hashtable 和 HashMap 的不同之處

  • Hashtable 的方法是同步的
    在這裏插入圖片描述

  • 父類是 Dictionary

  • 主數組長度不要求是 2 的冪

  • 默認長度 11

  • Key 和 value 都不能是 null

  • 計算哈希值就是直接調用 hashCode()

  • 計算存儲位置就是使用%取模
    在這裏插入圖片描述

  • 每次擴容爲原來容量的 2 倍再+1
    在這裏插入圖片描述

  • 使用 Enumeration 進行迭代,而不是使用 Iterator(JDK1.2 纔有)。

5.2 問題 7:爲什麼 Hashtable 主數組默認長度是 11,爲何擴容 2 倍還要+1

按照哈希表的經典理論:哈希表主數組的長度應該是一個素數,這樣會產生最分散的餘數,儘可能減少哈希衝突。11 就是素數。擴容 2 倍肯定不是素數,+1 可能變成素數。

5.3 問題 8:Hashtable 的缺點

Hashtable 的方法使用的 synchronized 同步方法鎖,非靜態方法的鎖是 this(當前的 Hashtable 對象,即整個哈希表)。一個線程上鎖,就會鎖住所有的訪問同步方法的線程,並且是擋在了方法之外,效率太低。
HashMap 是非線程同步的,可以藉助 Collections. synchronziedMap()保證線程安全,底層使用 synchronized 同步代碼塊,同步監視器也是當前的對象 this,也是鎖住了整個哈希表,但是是將其他線程鎖在了方法之內,同步代碼之外,實際性能比 Hashtable 有提高,但是提高有限。

在大量高併發情況下如何提高集合的效率和安全呢?能否降低鎖的粒度,鎖住哈希表的一部分而不是全部呢?
可以的!!!

  • JDK7 ConcurrentHashMap 鎖住主數組的一部分(幾個桶)
  • JDK8 ConcurrentHashMap 鎖住主數組的一部分(一個桶)

在這裏插入圖片描述

red:鎖住整個表(所有桶); blue:鎖住幾個桶 green:鎖住一個桶

六、JDK7 ConcurrentHashMap

6.1 問題 9:JDK7 ConcurrentHashMap 關鍵技能點

  • 使用分段鎖 Segment。由 Hashtable 的鎖住整個表,HashMap 的不鎖,到鎖住表的一部分。線程同步使用的是 Lock 鎖。
  • 結構圖:
    在這裏插入圖片描述
  • ConcurrentHashMap 是由 Segment 數組和 HashEntry 數組結構組成。
  • Segment 是一種可重入鎖 ReentrantLock,在 ConcurrentHashMap 裏扮演鎖的角色,HashEntry 則用於存儲鍵值對數據。
  • 一個 ConcurrentHashMap 裏包含一個 Segment 數組,Segment 的結構和 HashMap 類似,是一種數組和鏈表結構。一個 Segment 裏包含一個 HashEntry 數組,每個 HashEntry 是一個鏈表結構的元素。每個Segment 守護着一個 HashEntry 數組裏的元素,當對 HashEntry 數組的數據進行修改時,必須首先獲得它對應的 Segment 鎖。

6.2 問題 10:JDK7 ConcurrentHashMap 通過無參構造方法創建對象的結果在這裏插入圖片描述在這裏插入圖片描述

  • initialCapacity:初始容量,這個值指的是整個 ConcurrentHashMap 的初始容量,實際操作的時候需要平均分給每個 Segment
  • loadFactor:加載因子。Segment 數組不擴容,所以是針對每個 Segment內部的加載因子。
  • concurrencyLevel:併發級別,segment 數組容量,默認 16。要求是 2的冪。
  • SEGMENT_TABLE_CAPACITY:每個 Segment 的內部數組的容量,最小是 2。可以擴容,必須是 2 的冪。每次擴容 100%。(int newCapacity =oldCapacity << 1;)
  • 採用無參數構造方法創建對象後的內存結構如圖所示(不包含藍色部分)。Segment 數組長度 16,只給 segments[0]分配空間。發現每個 Segment其實就是一個HashMap,其中的數組 table 的容量是 2。
    在這裏插入圖片描述

認識 Segment 和 HashEntry:

  • Segment 其實就是之前的一個 HashMap,本身繼承了 ReentrantLock,可以直接複用加鎖、解鎖等操作
  • HashEntry 就是 HashMap 中的 Entry,是鏈表的節點類型,存儲具體的鍵值對信息。
  • 注意:Segment 的 table、HashEntry 的 value、next 都使用 volatile修飾,其修改在各個線程之間具有可見性。
    在這裏插入圖片描述

6.3 JDK7 ConcurrentHashMap 源碼閱讀

  • put 操作:
  1. 計算 key 所在的 Segments 數組的索引 j。如果 segments[j]==null,需要先分配空間。
    在這裏插入圖片描述

  2. 計算 key 所在的 HashEntry 數組的索引,並完成添加操作(使用 Lock 鎖保證併發安全)
    在這裏插入圖片描述

  3. 注 意 1 : 新 的 節 點 會 添 加 到 鏈 表 的 頭 部 ( JDK7 的 HashMap 和ConcurrentHashMap 都是添加到頭部)。

  4. 注意 2:添加了新節點再判斷是否擴容(JDK7 的 HashMap 是先判斷是否擴容,再添加)。

在這裏插入圖片描述

  • get 操作:
  1. 計算 key 所在的 Segments 數組的索引 j。如果是 null,直接返回 null,不存在。
  2. 計算 key 所在的 HashEntry 數組的索引。找到了返回 HashEntry 的value,找不到,返回 null。
  3. 查詢不加鎖,但是支持併發查詢。需要使用 UNSAFE 的方法
    在這裏插入圖片描述
  • size()操作:
  1. 有些方法需要跨段,比如 size()和 containsValue()。需要鎖定整個表而而不僅僅是某個段,這需要按順序鎖定所有段,操作完畢後,又按順序釋放所有段的鎖。
  2. 這裏“按順序”是很重要的,否則極有可能出現死鎖
    在這裏插入圖片描述

6.4 問題 11:Unsafe 類是怎麼回事

  • Unsafe 類是在 sun.misc 包下,不屬於 Java 標準。但是很多 Java 的基礎類庫,包括一些被廣泛使用的高性能開發庫都是基於 Unsafe 類開發的,比如Netty、Hadoop、Kafka 等。
  • 使用 Unsafe 可用來直接訪問系統內存資源並進行自主管理,大部分 API 都是 native 的方法。Unsafe 類在提升 Java 運行效率,增強 Java 語言底層操作能力方面起了很大的作用。
  • Unsafe 可認爲是 Java 中留下的後門,提供了一些低層次操作,如直接內存訪問、線程調度等。官方並不建議使用 Unsafe。

6.5 問題 12:JDK7 ConcurrentHashMap 的缺點是什麼

  • 結構複雜了:由一個 Segment 數組和多個 HashEntry 組成的兩級結構組成
  • 查詢效率低了:需要先查詢到 Segment 的索引,再查詢到 HashEntry 的索引。統計 size()需要遍歷整個 Segment 數組。
  • 鎖的粒度也不算小:concurrentLevel(併發數)基本上是固定的,其實還是鎖住了一個哈希表,哪怕是一個小的 HashMap。能否只鎖住 HashMap 的一個桶呢?concurrentLevel 就可以和數組大小保持一致了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章