Java集合框架————Map集合(2)Hashtable子類;ConcurrentHashMap子類

1.Hashtable子類

JDK1.0提供有三大主要類:Vector、Enumeration、Hashtable。Hashtable是最早實現這種二元偶對象數據結構,後期的設計也讓其與Vector一樣多實現了Map接口而已。

範例:觀察Hashtable

public class HashtableDemo {

    public static class TestDemo {
        public static void main(String[] args) {
            Map<Integer,String> map = new Hashtable<>() ;
            map.put(1,"hello") ;
// key重複
            map.put(1,"Hello") ;
            map.put(3,"Bad") ;
            map.put(2,"man") ;
            System.out.println(map);
        }
    }
}

在這裏插入圖片描述

以後使用的時候多考慮HashMap。

2. ConcurrentHashMap子類

爲什麼需要 ConcurrentHashMap?

Hashtable 本身比較低效,因爲它的實現基本就是將 put、get、size 等各種方法加上“synchronized”。簡單來說,這就導致了所有併發操作都要競爭同一把鎖,一個線程在進行同步操作時,其他線程只能等待,大大降低了併發操
作的效率。

那麼能不能利用 Collections 提供的同步包裝器來解決問題呢?
看看下面的代碼片段,我們發現同步包裝器只是利用輸入 Map 構造了另一個同步版本,所有操作雖然不再聲明成爲 synchronized 方法,但是還是利用了“this”作爲互斥的 mutex,沒有真正意義上的改進!

在這裏插入圖片描述

ConcurrentHashMap 分析

ConcurrentHashMap 的設計實現其實一直在演化,比如在 Java 8 中就發生了非常大的變化(Java 7 其實也有不少更新),所以,這裏將比較分析結構、實現機制等方面,對比不同版本的主要區別。

早期 ConcurrentHashMap,其實現是基於:

  • 分離鎖,也就是將內部進行分段(Segment),裏面則是HashEntry 的數組,和 HashMap類似,哈希相同的條目也是以鏈表形式存放。
  • HashEntry 內部使用 volatile 的 value 字段來保證可見性,也利用了不可變對象的機制以改進利用Unsafe 提供的底層能力,比如 volatile access,去直接完成部分操作,以最優化性能,畢竟 Unsafe 中的很多操作都是 JVM intrinsic 優化過的。

可以參考下面這個早期 ConcurrentHashMap 內部結構的示意圖,其核心是利用分段設計,在進行併發操作的時候,只需要鎖定相應段,這樣就有效避免了類似 Hashtable 整體同步的問題,大大提高了性能。

在這裏插入圖片描述

在構造的時候,Segment 的數量由所謂的 concurrency Level 決定,默認是 16,也可以在相應構造函數直接指定。注意,Java 需要它是 2 的冪數值,如果輸入是類似 15 這種非冪值,會被自動調整到 16 之類 2 的冪數值。

  • ConcurrentHashMap 會獲取再入鎖,以保證數據一致性,Segment 本身就是基於ReentrantLock 的擴展實現,所以,在併發修改期間,相應 Segment 是被鎖定的。
  • 在最初階段,進行重複性的掃描,以確定相應 key 值是否已經在數組裏面,進而決定是更新還是放置操作。重複掃描、檢測衝突是ConcurrentHashMap 的常見技巧。
  • 在 ConcurrentHashMap中擴容同樣存在。不過有一個明顯區別,就是它進行的不是整體的擴容,而是單獨對 Segment 進行擴容。

另外一個 Map 的 size 方法同樣需要關注,它的實現涉及分離鎖的一個副作用。
試想,如果不進行同步,簡單的計算所有 Segment 的總值,可能會因爲併發 put,導致結果不準確,但是直接鎖定所有 Segment 進行計算,就會變得非常昂貴。其實,分離鎖也限制了 Map的初始化等操作。
所以,ConcurrentHashMap 的實現是通過重試機制(RETRIES_BEFORE_LOCK,指定重試次數 2),來試圖獲得可靠值。如果沒有監控到發生變化(通過對比 Segment.modCount),就直接返回,否則獲取鎖進行操作。

下面來對比一下,在 Java 8 和之後的版本中,ConcurrentHashMap 發生了哪些變化呢?

  • 總體結構上,它的內部存儲變得和HashMap 結構非常相似,同樣是大的桶(bucket)數組,然後內部也是一個個所謂的鏈表結構(bin),同步的粒度要更細緻一些。
  • 其內部仍然有 Segment 定義,但僅僅是爲了保證序列化時的兼容性而已,不再有任何結構上的用處。
  • 因爲不再使用 Segment,初始化操作大大簡化,修改爲 lazy-load 形式,這樣可以有效避免初始開銷,解決了老版本很多人抱怨的這一點。
  • 數據存儲利用 volatile 來保證可見性。
  • 使用 CAS 等操作,在特定場景進行無鎖併發操作。
  • 使用 Unsafe、LongAdder 之類底層手段,進行極端情況的優化。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章