生動有趣帶你看JDK1.8-ConcurrentHashMap的put源碼

上一節說到hashmap的源碼,那麼這一節來說說ConcurrentHashMap的源碼,因爲ConcurrentHashMap是線程安全版多的hashmap,所以看這節最好先看hashmap源碼那一節,所以在這裏給你們準備好了傳送門

生動有趣帶你看JDK1.8-HashMap的put源碼,看完直呼原來這麼簡單!

HashMap在多線程的情況下操作可能會和單線程操作情況數據不一致,就會出現所謂的線程安全的問題,線程安全就會想到的做法是加鎖,也就是HashTable,但是加鎖性能太差了,爲什麼這樣說呢,HashTable直接把鎖加在了put方法上,一個操作只能讓一個線程進入,必然性能變差。

那採用無鎖化呢?就衍生出了ConcurrentHashMap,ConcurrentHashMap採用CAS,那麼什麼是CAS呢?

CAS:保證對某個線程安全,獲取對操作對象的新值和你需要修改的值,兩者進行比較,如果相等就證明是安全的,如果不相等就證明被其他線程改過了,那麼你就不能進行改動。

首先套路還是一樣,先縷清成員變量,其實是和HashMap一樣的數據結構,都是需要有數組的,但是不同的是定義的時候加了volatile可見性這樣的單詞,代表這個成員變量是要可見的,因爲是要進行多線程操作了麼

1. 數組初始化

那麼看源碼之前想一下HashMap的操作哪裏不安全?數組初始化是否安全、存儲數據是否安全?

假設多線程模式不加鎖,T1線程進來進行了初始化要存儲數據了,T2此時也進來了然後進行了初始化操作,那麼就亂了,到底初始化還是存儲數據,容易出現數據混亂的問題,所以數組初始化是要保證安全的,這樣一想存儲數據就一定需要加鎖。

那源碼來看一下是不是這樣的操作,還是熟悉的putVal方法,剛進來tab一定是null,所以進入初始化。

進入initTable初始化方法,映入眼簾的是一個判斷,sc是局部變量默認爲0,那麼sizeCtl是在哪裏定義的呢?(查看下圖第二個)原來也是定義的成員變量,那麼第一次sizeCtl是等於0的,把0賦值給了sc,判斷以後一定不小於0。

不小於0就會進入另一個判斷,發現這裏加了CAS了,關鍵性的一幕來了U.compareAndSwapInt(this, SIZECTL, sc, -1)它來了,這句話的意思判斷SIZECTL和sc是否相等,那麼看下SIZECTL,如下圖(第二個)定義的,發現也是定義的變量沒有進行賦值默認爲0,所以SIZECTL=0,sc=0說明數一致可以進入方法,進行數組初始化了。那麼數不一致sc被賦值成-1,(sc=sizeCtl)<0就成立了,線程就會作出讓步Thread.yield()。

所以ConcurrentHashMap對數組的初始化使用了CAS無鎖化的方式保證了線程安全。

2.存儲數據

那除了這塊需要加上同步還有哪裏需要加上呢,是不是put存儲數據的時候也需要加上,進入putVal方法看源碼,初始化過後進行hash計算以後,那麼又進行了CAS的判斷在第一次進入的時候i是爲null,判斷當i和null的比對以後纔可以創建節點。

初始化完成,也能創建節點並且都能保證線程安全。那麼接下來就是進入節點是有值的要麼進行鏈表操作,要麼進行紅黑樹操作,要麼Key值相等,進行put更新操作

下圖是Key值相等,進行put更新

判斷下一個節點是否有值,next沒值進行存儲,鏈表存儲操作

樹結構存儲的操作

那麼以上這些存儲節點的操作是不是需要加鎖呢?答案是要的,需要注意的是這裏不使用CAS了,這裏就變成了synchronized,爲什麼這裏使用了synchronized而不用CAS了呢,因爲CAS需要進行比對,節點太多一個一個比對比較麻煩,而這裏用了synchronized,是因爲巧妙的用了synchronized的作用域,可以看一下synchronized方法裏傳遞的f是什麼,f就是當前的節點,也就是說作用域就是鎖住當前節點,其他put操作算出來的hash只要不是數組同一個位置就不會進行鎖操作,主要減少鎖的粒度,就增加了性能。

3.數據擴容

那麼以上都講完了,是不是還缺個功能,對的就是擴容了,先說一下擴容需不需要線程安全,答案是一定的,我第一個線程在搬數據,第二個也在般數據,那麼不就是亂套了麼,還搬的是一個。

那麼這裏說一下,這裏擴容不是鎖住,它會讓其他線程幫助那個正在搬運的線程,也就是說當t1線程正在擴容,t2線程進來就不能put了,而是領取任務幫忙搬運。怎麼個搬運法呢,假如擴容64個長度,那麼t1正在搬運前16個,那麼就把另一個長度16搬運的工作交給T2,來都來了別閒着哈哈哈,T3線程進來發現還沒擴容完繼續幫忙領取任務再給16長度的,直到都擴容完成!

 

先看一下擴容源碼f.hash頭節點==MOVED,MOVED就是下圖默認給-1,這裏你只需要知道,達到擴容的條件就會給hash賦值爲-1

做一些複雜的操作方法,最後有條件就進行擴容,那麼這裏就是用了addCount方法判斷是否擴容,當然addCount方法裏的代碼有點多,但是主幹就是達到條件就會去擴容,然後都會去transfer方法。

進入transfer方法,大家都會是先領取任務,stride是步伐的意思,MIN_TRANSFER_STRIDE全局變量是多少呢?下圖MIN_TRANSFER_STRIDE是16,也就是判斷當前數組小於16個,如果小於就不用其他線程去幫忙搬了,我一個人就行了。

如果數組大小是64個,我需要put元素,但是發現其他的線程在擴容,那麼我此刻就是需要去幫忙搬元素就進入helpTransfer方法

helpTransfer方法裏也是進行一系列的判斷,最終還是進入transfer方法裏進行幫忙,進入到transfer以後就要去領取第二個階段的16個數組大小的任務,而真正核心的擴容步驟是和HashMap是一樣的。

你會發現transfer方法的源碼,前部分是進行一個線程任務領取

transfer方法的源碼,中間部分是不斷的判斷自己的任務到底是否完成,如果完成做個標記,如果全部完成就統統返回

後部就是搬運數據的過程

那麼除了加鎖啊,cas判斷啊,在存儲數據操作其實是和hashMap是一樣的。

那麼ConcurrentHashMap主要的部分就說完了,本博客是學習咕泡學院的-jack老師的公開課講解的進行整理總結,碼字不易,給個贊再走被!希望本博客能夠幫助你!

加下關注更感激不盡了,我會持續奮進學習書籍、視頻,然後記錄博客,讓大家都能一起學習到!筆心

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