ConcurrentHashMap源碼剖析

前言

鎖優化有一個很重要的思路,就是拆分鎖的粒度,類似於分佈式鎖優化的實踐,把一份數據拆分爲多份數據,對每一份數據片段來加鎖,這樣就可以提升多線程併發的效率

HashMap底層不就是一個數組的數據結構麼?如果你要完全保證裏的併發安全,如果你每次對數組做一些put、resize、get的操作的時候,你都是加鎖,synchronized好了,此時就會導致併發性能非常的低下

所有的線程讀寫hashmap的過程都是串行化的,hashtable,就是採用的這種做法

讀寫鎖,大量的讀鎖和寫鎖衝突的時候,也會導致多線程併發的效率大大的降低,也不行。

ConcurrentHashMap,分段加鎖,把一份數據拆分爲多個segment,對每個段設置一把小鎖,put操作,僅僅只是鎖掉你的那個數據一個segment而已,鎖一部分的數據,其他的線程操作其他segmetn的數據,跟你是沒有競爭的。大大減少了鎖競爭,從而提高了性能。

put()剖析

int hash = spread(key.hashCode());

對key獲取了hashCode,調用了spread算法,獲取到了一個hash值

    static final int spread(int h) {

        return (h ^ (h >>> 16)) & HASH_BITS;

}

他相當於是把hash值的高低16位都考慮到後面的hash取模算法裏,這樣就可以把hash值的高低16位的特徵都放到hash取模算法來運算,有助於儘可能打散各個key在不同的數組的位置,降低hash衝突的概率

剛開始,table是null的話,此時就要初始化這個table。

U.compareAndSwapInt(this, SIZECTL, sc, -1):CAS操作,sizeCtl = -1

初始化一個table數組,默認的大小就是16

tabAt(tab, i = (n - 1) & hash) --》return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);

i = (n - 1) & hash,這個就是hash取模的算法,定位的算法。定位出來的位置,傳遞給了tabAt()函數進行volatile讀。

他在這裏的意思,就是走一個線程安全的操作,Unsafe,如果此時數組那個位置的元素給返回出來。

當前這個數組這裏沒有元素的話,此時就直接把你的key-value對放在數組的這個地方了。

通過casTabAt()--》 U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);

通過上述的代碼,使用底層的CAS操作,直接將你的key-value對放在了數組的hash定位到的那個位置。

爲什麼ConcurrentHashMap他說是線程安全的呢?

Unsafe,CAS的操作,依靠線程安全的CAS對數組裏的位置進行賦值,只有一個線程可以在這裏成功的將一個key-value對放在數組的一個地方里。

CAS初步體現了分段加鎖的思想???

他並沒有對整個一個大的數組進行synchronized加鎖的機制,並沒有,線程都會串行化的來執行,性能和效率是很低下的。

多個線程同時CAS操作數組同一個位置的元素時,在硬件層面實際上會對這個元素加了一個鎖,此時只有一個線程可以加鎖成功,其他線程則等待他釋放鎖後纔可以進行CAS操作。數組中的每一個位置實際上都可以加一把CAS鎖,沒有對一個數組全部進行加鎖,僅僅是說對數組的同一個位置的元素賦值的時候,多個線程會基於CAS(隱含式的加鎖),僅僅是對數組的那個位置的元素進行加鎖而已,隱式的加鎖,硬件層面上的鎖。

數組大小是16,有16個元素,同時可以允許最多是16個線程同時併發的來操作這個數組,如果16個線程操作的是數組的不同位置的元素的話,此時16個線程之間是沒有任何鎖的關係

數組大小是16,16個元素,CAS賦值的機制,實現了一個效果,這個數組有16把鎖,每個元素是一把鎖,只有競爭同一個位置的元素的多個線程,纔會對一把鎖進行爭用的操作。

CAS成功和失敗

如果CAS成功,key-value對就直接進到數組裏去了。

如果CAS失敗了以後,兩個或者是多個線程同時來進行併發的put操作的時候,如果不巧的定位都到了一個數組的元素的位置,此時大家都嘗試進行CAS,只有一個線程是可以執行成功的,而其他的線程此時CAS設置數組的元素就會失敗。

萬一有線程CAS失敗了呢?此時會就什麼都不會幹,直接進入下一輪for循環,(f = tabAt(tab, i = (n - 1) & hash)) == null,此時會發現說數組的那個位置的元素已經不是null了,因爲之前已經有人在數組的那個位置設置了一個Node。此時就應該對數組的那個位置的元素進行鏈表+紅黑樹的處理,把衝突的多個key-value對掛成一個鏈表或者是紅黑樹。

f就代表了數組當前位置的元素,Node節點

synchronized(f) {

。。。。

}

這個就是所謂的JDK 1.8裏的ConcurrentHashMap分段加鎖的思想,淋漓盡致的體現,他僅僅就是對數組這個位置的元素加了一把鎖而已。同一時間只有一個線程可以進鎖來進行這個位置的鏈表+紅黑樹的處理,這個就是分段加鎖

數組的中的每一個元素,無論是null情況下來賦值(CAS,分段加鎖思想),有值情況下來處理鏈表/紅黑樹(synchronized,對元素本身加鎖,更加是分段加鎖思想),都是對數組的一個元素加鎖而已。你的數組有多大,有多少個元素,你就有多少把鎖,大幅度的提升了整個HashMap加鎖的效率。

如果有一個線程成功對數組的某個元素加鎖了,synchronized.如何對這個位置的元素進行鏈表/紅黑樹的處理?

                                if (e.hash == hash &&

                                    ((ek = e.key) == key ||

                                     (ek != null && key.equals(ek)))) {

                                    oldVal = e.val;

                                    if (!onlyIfAbsent)

                                        e.val = value;

                                    break;

                                }

如果發現說我當前要put的key跟數組裏這個位置的key是一樣的,此時就對數組當前位置的元素的value值覆蓋一下

如果兩個key不同,就默認走鏈表的一個處理,此時就是把e.next = 新封裝的一個節點,如下代碼所示,e就是數組當前位置的元素

    Node<K,V> pred = e;

                                if ((e = e.next) == null) {

                                    pred.next = new Node<K,V>(hash, key,

                                                              value, null);

                                    break;

                                }

 

                if (binCount != 0) {

                    if (binCount >= TREEIFY_THRESHOLD)

                        treeifyBin(tab, i);

                    if (oldVal != null)

                        return oldVal;

                    break;

                }

上面這塊代碼的判斷就是,如果一個鏈表的元素的數量超過了8,達到了一個閾值之後,就會將鏈表轉換爲紅黑樹。如果轉換爲紅黑樹以後,下次如果有hash衝突的問題,是直接把key-value對加入到紅黑樹裏去   

   else if (f instanceof TreeBin) {

                            Node<K,V> p;

                            binCount = 2;

                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,

                                                           value)) != null) {

                                oldVal = p.val;

                                if (!onlyIfAbsent)

                                    p.val = value;

                            }

                        }

出現hash衝突的時候,分段加鎖成功了以後,就會做值覆蓋/鏈表/紅黑樹處理,前提條件,都是你對數組當前位置的元素synchronized加鎖成功了纔可以的。

get()剖析

不加鎖,但是他通過volatile讀,儘可能給你保證說是讀到了其他線程修改的一個最新的值,但是不需要加鎖。

volatile底層硬件級別的原理,volatile讀操作的話,會插入一個load屏障,絕對是在讀取數據的時候,一定會嗅探一下,探查一下無效隊列,這個數據是否被別人修改過了。

此時必須立馬過期掉本地高速緩存裏的緩存數據,invalid(I),然後再讀的時候,就需要發送read消息到總線,從其他線程修改修改這個值的線程的高速緩存裏(或者從主存讀。具體從哪裏讀取最新的值,跟CPU有關係),必須這個加載到最新的值

size()剖析

size方法,是幫助你去讀到當前最新的一份數據,通過volatile的讀操作。但是因爲讀操作是不加鎖,他不定根可以保證說,你讀的時候就一定沒人修改了,很可能是你剛剛讀完一份數據,就有人來修改

這個數據結構,記錄了數組的每個位置掛載了多少個元素,每個位置都可能掛的是鏈表或者是紅黑樹,此時可能一個位置有多個元素。注意:value值是volatile變量,保證可見性。

將每個位置對應的counterCell累加起來,再加上baseCount就是我們map的總大小。

JDK1.7和JDK1.8中ConcurrentHashMap的對比

JDK 1.7的ConcurrentHashMap,稍微是有一些差距的。Segment,16個Segment,16個分段,每個Segment對應Node[]數組,每個Segment有一把鎖,也就是說對一個Segment裏的Node[]數組的不同的元素如果要put操作的話,其實都是要競爭一個鎖,串行化來處理的。鎖的粒度是比較粗的,因爲一個Node[]數組是一把鎖,但是他有多個Node[]數組

JDK 1.8的ConcurrentHashMap,優化了,鎖粒度細化,他是就一個Node[]數組,正常會擴容的,但是他的鎖粒度是針對的數組裏的每個元素,每個元素的處理會加一把鎖,不同的元素就會有不同的鎖。大幅度的提升了多線程併發寫ConcurrentHashMap的性能,降低了鎖的衝突。

 

 

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