ConcurrentHashMap 實現原理

由於 HashMap 是一個線程不安全的容器,主要體現在容量大於總量*負載因子發生擴容時會出現環形鏈表從而導致死循環。

因此需要支持線程安全的併發容器 ConcurrentHashMap 。

(1)JDK1.7 實現

數據結構

由 Segment 數組、HashEntry 數組組成,和 HashMap 一樣,仍然是數組加鏈表組成。

ConcurrentHashMap 採用了分段鎖技術,其中 Segment 繼承於 ReentrantLock。不會像 HashTable 那樣不管是 put 還是 get 操作都需要做同步處理,理論上 ConcurrentHashMap 支持 CurrencyLevel (Segment 數組數量)的線程併發。每當一個線程佔用鎖訪問一個 Segment 時,不會影響到其他的 Segment

get 方法

ConcurrentHashMap 的 get 方法是非常高效的,因爲整個過程都不需要加鎖。

只需要將 Key 通過 Hash 之後定位到具體的 Segment ,再通過一次 Hash 定位到具體的元素上。由於 HashEntry 中的 value 屬性是用 volatile 關鍵詞修飾的,保證了內存可見性,所以每次獲取時都是最新值。

put 方法

內部 HashEntry 類 :

static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;

        HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }

雖然 HashEntry 中的 value 是用 volatile 關鍵詞修飾的,但是並不能保證併發的原子性,所以 put 操作時仍然需要加鎖處理。

首先也是通過 Key 的 Hash 定位到具體的 Segment,在 put 之前會進行一次擴容校驗。

這裏比 HashMap 要好的一點是:

HashMap 是插入元素之後再看是否需要擴容,有可能擴容之後後續就沒有插入就浪費了本次擴容(擴容非常消耗性能)。

而 ConcurrentHashMap 不一樣,它是在將數據插入之前檢查是否需要擴容,之後再做插入操作。

size 方法

每個 Segment 都有一個 volatile 修飾的全局變量 count ,求整個 ConcurrentHashMap 的 size 時很明顯就是將所有的 count 累加即可。但是 volatile 修飾的變量卻不能保證多線程的原子性,所有直接累加很容易出現併發問題。

但如果每次調用 size 方法將其餘的修改操作加鎖效率也很低。所以做法是先嚐試兩次將 count 累加,如果容器的 count發生了變化再加鎖來統計 size

至於 ConcurrentHashMap 是如何知道在統計時大小發生了變化呢,每個 Segment 都有一個 modCount 變量,每當進行一次 put remove 等操作,modCount 將會 +1。只要 modCount 發生了變化就認爲容器的大小也在發生變化。

 

(2)JDK1.8 實現

1.8 中的 ConcurrentHashMap 數據結構和實現與 1.7 還是有着明顯的差異。

其中拋棄了原有的 Segment 分段鎖,而採用了 CAS + synchronized 來保證併發安全性。

也將 1.7 中存放數據的 HashEntry 改爲 Node,但作用都是相同的。

其中的 val next 都用了 volatile 修飾,保證了可見性。

put 方法

  • 根據 key 計算出 hashcode 。
  • 判斷是否需要進行初始化。
  • f 即爲當前 key 定位出的 Node,如果爲空表示當前位置可以寫入數據,利用 CAS 嘗試寫入,失敗則自旋保證成功。
  • 如果當前位置的 hashcode == MOVED == -1,則需要進行擴容。
  • 如果都不滿足,則利用 synchronized 鎖寫入數據。
  • 如果數量大於 TREEIFY_THRESHOLD 則要轉換爲紅黑樹。

get 方法

  • 根據計算出來的 hashcode 尋址,如果就在桶上那麼直接返回值。
  • 如果是紅黑樹那就按照樹的方式獲取值。
  • 都不滿足那就按照鏈表的方式遍歷獲取值。

 

總結

1.8 在 1.7 的數據結構上做了大的改動,採用紅黑樹之後可以保證查詢效率(O(logn)),甚至取消了 ReentrantLock 改爲了 synchronized,這樣可以看出在新版的 JDK 中對 synchronized 優化是很到位的。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章