HashMap,HashTable,HashSet,ConcurrentHashMap 區別,ConcurrentSkipListMap(跳錶)

一. HashSet 與 HashMap 的區別

  1. HashMap 實現了 Map 接口,存儲的是鍵值對,而HashSet 實現的是 Set 接口,底層是基於 HashMap 實現的,也是鍵值對形式,但 value 的值都爲空。
  2. HashMap 使用 put 方法將元素放入 map 中,HashSet 使用 add 方法將元素放入 Set 中。
  3. 如果兩個對象的 HashCode 相同,就採用 equals 方法來判斷對象的相等性,不能有重複數據。
HashSet 如何檢查重複?

當把對象加入 HashSet 時,會先通過對象的 hashcode 值來判斷要加入的位置,同時也會與其他加入對象的 hashcode 值作比較,如果沒有相同的 hashcode,則加入成功。如果有相同hashcode值的對象,就調用 equals 方法來檢查它們的值是否相同。如果相同,則加入失敗。

== 與 equals 的區別
  • == 是判斷兩個變量是不是指向同一個內存空間; equals是判斷兩個變量所指向的內存空間的值是不是相同。
  • ==是指對內存地址進行比較 ;equals() 是對字符串的內容進行比較。
  • ==指引用是否相同 ;equals() 指的是值是否相同。

二. ConcurrentHashMap 與 HashMap 的區別

JDK1.7 —— 分段鎖

  JDK1.7 中 ConcurrentHashMap 底層實現和 JDK1.7 的 HashMap 類似,也是採用了數組 + 鏈表的結構。同時還採用了分段鎖 segment ,segment 繼承 ReentrantLock。一個 ConcurrentHashMap 裏包含一個 Segment 數組。Segment 的結構和 HashMap 類似,也是一種數組加鏈表的結構,Segment 數組中每一個 Segment 包含一個 HashEntry 數組,HashEntry 是一個鏈表結構,當對 HashEntry 數組的數據進行修改時,必須先獲得對應 Segment 的鎖。多個線程可以同時訪問不同 Segment 上的 HashEntry,從而使併發度提高,併發度與 Segment 的數量相等。

JDK1.8 —— Synchronized 和 CAS 操作
  • JDK1.8 中 ConcurrentHashMap 底層實現和 JDK1.8 的HashMap 類似,也是採用了數組+鏈表+紅黑樹的結構,鏈表採用尾插法插入數據,鏈表長度超過閾值8時轉換爲紅黑樹,小於閾值6時轉換爲鏈表。併發控制改成使用 Synchronized 和 CAS 操作,Synchronized 只鎖定當前鏈表或紅黑樹的首節點,相比 JDK1.7 中鎖住包含多個 HashEntry 的 Segment,鎖粒度明顯降低。
  • put 過程: 寫入數據時如果桶爲空,則通過 CAS 操作插入元素,如果桶不爲空,且當前節點不處於移動狀態,也就是 hash 值不等於值爲 - 1 的 moved 值,就對該節點加 Synchronized 鎖來寫入數據。
  • get 過程: HashEntry 中的 val 屬性用 volatile 關鍵詞修飾,保證了內存可見性,所以每次獲取時都是最新值,因此 ConcurrentHashMap 的 get 方法不需要加鎖,非常高效。

ConcurrentHashMap 與 HashMap 的區別

  1. JDK1.8以前底層都是數組+鏈表結構,鏈表採用頭插法,JDK1.8中都引入了紅黑樹,鏈表改爲尾插法。(HashTable 是數組+鏈表,頭插法)
  2. HashMap 是線程不安全的,因爲它的 get 和 put 方法都沒有加鎖,在多線程環境下無法保證上一秒 put 的值,在下一秒 get 的時候還是原值,所以線程安全依然無法保證。ConcurrentHashMap 在JDK1.7中把整個數組分成若干個 Segment ,然後通過 ReentrantLock 對每個 Segment 單獨加鎖;1.8中通過 Synchronized 和 CAS 機制來保證線程安全。(HashTable 通過 Synchronized 來保證線程安全)
  3. ConcurrentHashMap 的鍵值不能爲 null ,hashMap 的可以爲 null。因爲 ConcurrentHashMap 在多線程的環境中,如果 get 方法得到的是 null,並不能判斷是映射的 value 爲null,還是因爲沒有找到對應的 key 而爲 null,系統無法判斷出這種模糊不清的情況。而用於單線程的 hashMap 可以通過 containsKey 方法來判斷到底是否包含這個 null。(HashTable 也不能爲 null)
  4. HashMap 和 ConcurrentHashMap 的初始容量都是16,都採取兩倍的擴容方式。(HashTable 初始容量是11,採取兩倍的原始容量+1 的方式進行擴容)

hashMap 和 ConcurrentHashMap 的 size 操作

  • HashMap 定義了一個 modCount 變量,每次變動時,無論是 put 還是 remove ,都將 modCount 加1。遍歷兩次數組,如果得出的 modCount 值一樣,就表示未變動了,成功返回 size。否則就表示又變動過了,就繼續遍歷再次比較 modCount。
  • ConcurrentHashMap 1.7:首先以不加鎖的方式,定義了一個 modCount 變量,每次變動時,無論是 put 還是 remove ,都將 modCount 加1。遍歷兩次數組,如果得出的 modCount 值一樣,就表示未變動了,成功返回 size。否則就再遍歷一次,如果依然不一致,就對所有的 Segment 加鎖,然後一個一個遍歷來求出 size。
  • ConcurrentHashMap 1.8:新值了 mappingCount 方法,它的返回值類型是 long 類型,不會因爲 size 方法是 int 類型而限制最大值。它裏面有一個 volatile 修飾的 baseCount 變量,當沒有發生衝突時,使用 baseCount 來計數, 還有一個填充單元 CounterCell 數組,當併發產生衝突時,使用 CounterCell 計數,最後通過 baseCount 和 CounterCell 數組總的計數值來得到 size 大小。其中 CounterCell 添加了 @Contended 註解來防止僞共享,僞共享產生的原因是因爲緩存系統是以緩存行爲單位進行存儲的,緩存行是 2 的冪次方的連續字節,一般爲64個字節,當多線程修改同一個緩存行中不同的變量時,由於同時只能有一個線程操作緩存行,所以會影響彼此的性能。JDK1.8 通過添加註解 @Contended 使變量在緩存行中分隔開來解決僞共享問題。(在JDK1.8之前是通過數據填充的方式來解決)(得出的 size 並不準確)
      

三. ConcurrentSkipListMap(跳錶)

  • ConcurrentSkipListMap是基於跳錶實現的,跳錶是一種鏈表加多層索引的結構,支持快速的插入、刪除、查找操作,時間複雜度都是O(logn)。空間複雜度爲O(n)。
  • 查找時,從頂級鏈表開始找。一旦發現被查找的元素大於當前鏈表中的元素,就會轉入下一層鏈表繼續查找。查找過程是跳躍式的。跳錶是一種利用空間換時間的算法。
  • 跳錶內所有的元素都是排序的。因此在對跳錶進行遍歷時,可以得到一個有序的結果。(哈希表不會保存元素的順序)
  • 跳錶結構和平衡樹類似,它和平衡樹的區別是:對平衡樹的插入和刪除可能導致平衡樹進行一次全局的調整。而對跳錶的插入和刪除只需要對數據結構的局部進行操作即可。這樣帶來的好處是:在高併發的情況下,需要一個全局鎖來保證整個平衡樹的線程安全。而對於跳錶,需要局部鎖即可,這樣在高併發環境下,就可以擁有更好的性能。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章