ConcurrentHashMap爲什麼支持完全併發的讀

ConcurrentHashMap完全允許多個讀操作併發進行,讀操作並不需要加鎖。(事實上,ConcurrentHashMap支持完全併發的讀以及一定程度併發的寫。)如果使用傳統的技術,如HashMap中的實現,如果允許可以在hash鏈的中間添加或刪除元素,讀操作不加鎖將得到不一致的數據。但是ConcurrentHashMap實現技術是保證HashEntry幾乎是不可變的。HashEntry代表每個hash鏈中的一個節點,其結構如下所示: 

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

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

	@SuppressWarnings("unchecked")
	static final <K,V> HashEntry<K,V>[] newArray(int i) {
	    return new HashEntry[i];
	}
    }

可以看到除了value不是final的,其它值都是final的,這意味着不能從hash鏈的中間或尾部添加或刪除節點,因爲這需要修改next引用值,所有的節點的修改只能從頭部開始。對於put操作,可以一律添加到Hash鏈的頭部。但是對於remove操作,可能需要從中間刪除一個節點,這就需要將要刪除節點的前面所有節點整個複製一遍,最後一個節點指向要刪除結點的下一個結點。爲了確保讀操作能夠看到最新的值,將value設置成volatile,這避免了加鎖。 remove操作要注意一個問題:如果某個讀操作在刪除時已經定位到了舊的鏈表上,那麼此操作仍然將能讀到數據,只不過讀取到的是舊數據而已,這在多線程裏面是沒有問題的。
HashEntry 類的 value 域被聲明爲 Volatile 型,Java 的內存模型可以保證:某個寫線程對 value 域的寫入馬上可以被後續的某個讀線程“看”到。在 ConcurrentHashMap 中,不允許用 null作爲鍵和值,當讀線程讀到某個 HashEntry 的 value 域的值爲 null 時,便知道產生了衝突——發生了重排序現象,需要加鎖後重新讀入這個 value 值。這些特性互相配合,使得讀線程即使在不加鎖狀態下,也能正確訪問 ConcurrentHashMap。 

在看源碼實現時,對HashEntry 的 value 域的值可能爲 null有些疑惑,網上都是說發生了重排序現象,後來仔細想想不完全正確,重排序發生在刪除操作時,這只是其中的一個原因,儘管ConcurrentHashMap不允許將value爲null的值加入,但現在仍然能夠讀到一個爲空的value就意味着此值對當前線程還不可見,主要因爲HashEntry還沒有完全構造完成導致的,所以對添加和刪除(對鏈表的結構性修改都可能會導致value爲null)。


ps:Hashtable,Collections.synchronizedMap,ConcurrentHashMap的實現的差異可參考http://hellosure.iteye.com/blog/1143942

發佈了86 篇原創文章 · 獲贊 23 · 訪問量 36萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章