一. HashSet 與 HashMap 的區別
- HashMap 實現了 Map 接口,存儲的是鍵值對,而HashSet 實現的是 Set 接口,底層是基於 HashMap 實現的,也是鍵值對形式,但 value 的值都爲空。
- HashMap 使用 put 方法將元素放入 map 中,HashSet 使用 add 方法將元素放入 Set 中。
- 如果兩個對象的 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 的區別
- JDK1.8以前底層都是數組+鏈表結構,鏈表採用頭插法,JDK1.8中都引入了紅黑樹,鏈表改爲尾插法。(HashTable 是數組+鏈表,頭插法)
- HashMap 是線程不安全的,因爲它的 get 和 put 方法都沒有加鎖,在多線程環境下無法保證上一秒 put 的值,在下一秒 get 的時候還是原值,所以線程安全依然無法保證。ConcurrentHashMap 在JDK1.7中把整個數組分成若干個 Segment ,然後通過 ReentrantLock 對每個 Segment 單獨加鎖;1.8中通過 Synchronized 和 CAS 機制來保證線程安全。(HashTable 通過 Synchronized 來保證線程安全)
- ConcurrentHashMap 的鍵值不能爲 null ,hashMap 的可以爲 null。因爲 ConcurrentHashMap 在多線程的環境中,如果 get 方法得到的是 null,並不能判斷是映射的 value 爲null,還是因爲沒有找到對應的 key 而爲 null,系統無法判斷出這種模糊不清的情況。而用於單線程的 hashMap 可以通過 containsKey 方法來判斷到底是否包含這個 null。(HashTable 也不能爲 null)
- 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)。
- 查找時,從頂級鏈表開始找。一旦發現被查找的元素大於當前鏈表中的元素,就會轉入下一層鏈表繼續查找。查找過程是跳躍式的。跳錶是一種利用空間換時間的算法。
- 跳錶內所有的元素都是排序的。因此在對跳錶進行遍歷時,可以得到一個有序的結果。(哈希表不會保存元素的順序)
- 跳錶結構和平衡樹類似,它和平衡樹的區別是:對平衡樹的插入和刪除可能導致平衡樹進行一次全局的調整。而對跳錶的插入和刪除只需要對數據結構的局部進行操作即可。這樣帶來的好處是:在高併發的情況下,需要一個全局鎖來保證整個平衡樹的線程安全。而對於跳錶,需要局部鎖即可,這樣在高併發環境下,就可以擁有更好的性能。