精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

Java Map中那些巧妙的設(shè)計

開發(fā) 開發(fā)工具
最近拜讀了一些Java Map的相關(guān)源碼,不得不驚嘆于JDK開發(fā)者們的鬼斧神工。他山之石可以攻玉,這些巧妙的設(shè)計思想非常有借鑒價值,可謂是最佳實踐。

最近拜讀了一些Java Map的相關(guān)源碼,不得不驚嘆于JDK開發(fā)者們的鬼斧神工。他山之石可以攻玉,這些巧妙的設(shè)計思想非常有借鑒價值,可謂是最佳實踐。然而,大多數(shù)有關(guān)Java Map原理的科普類文章都是專注于“點”,并沒有連成“線”,甚至形成“網(wǎng)狀結(jié)構(gòu)”。因此,本文基于個人理解,對所閱讀的部分源碼進行了分類與總結(jié),歸納出Map中的幾個核心特性,包括:自動擴容、初始化與懶加載、哈希計算、位運算與并發(fā),并結(jié)合源碼進行深入講解,希望看完本文的你也能從中獲取到些許收獲(本文默認(rèn)采用JDK1.8中的HashMap)。

一 自動擴容

最小可用原則,容量超過一定閾值便自動進行擴容。

擴容是通過resize方法來實現(xiàn)的。擴容發(fā)生在putVal方法的最后,即寫入元素之后才會判斷是否需要擴容操作,當(dāng)自增后的size大于之前所計算好的閾值threshold,即執(zhí)行resize操作。

??

??

 

通過位運算<<1進行容量擴充,即擴容1倍,同時新的閾值newThr也擴容為老閾值的1倍。

??

??

 

擴容時,總共存在三種情況:

  • 哈希桶數(shù)組中某個位置只有1個元素,即不存在哈希沖突時,則直接將該元素copy至新哈希桶數(shù)組的對應(yīng)位置即可。
  • 哈希桶數(shù)組中某個位置的節(jié)點為樹節(jié)點時,則執(zhí)行紅黑樹的擴容操作。
  • 哈希桶數(shù)組中某個位置的節(jié)點為普通節(jié)點時,則執(zhí)行鏈表擴容操作,在JDK1.8中,為了避免之前版本中并發(fā)擴容所導(dǎo)致的死鏈問題,引入了高低位鏈表輔助進行擴容操作。

??

??

 

在日常的開發(fā)過程中,會遇到一些bad case,比如:

HashMap hashMap = new HashMap(2); 
hashMap.put("1", 1);
hashMap.put("2", 2);
hashMap.put("3", 3);

當(dāng)hashMap設(shè)置最后一個元素3的時候,會發(fā)現(xiàn)當(dāng)前的哈希桶數(shù)組大小已經(jīng)達到擴容閾值2*0.75=1.5,緊接著會執(zhí)行一次擴容操作,因此,此類的代碼每次運行的時候都會進行一次擴容操作,效率低下。在日常開發(fā)過程中,一定要充分評估好HashMap的大小,盡可能保證擴容的閾值大于存儲元素的數(shù)量,減少其擴容次數(shù)。

二 初始化與懶加載

初始化的時候只會設(shè)置默認(rèn)的負(fù)載因子,并不會進行其他初始化的操作,在首次使用的時候才會進行初始化。

當(dāng)new一個新的HashMap的時候,不會立即對哈希數(shù)組進行初始化,而是在首次put元素的時候,通過resize()方法進行初始化。

??

??

 

resize()中會設(shè)置默認(rèn)的初始化容量DEFAULT_INITIAL_CAPACITY為16,擴容的閾值為0.75*16 = 12,即哈希桶數(shù)組中元素達到12個便進行擴容操作。

最后創(chuàng)建容量為16的Node數(shù)組,并賦值給成員變量哈希桶table,即完成了HashMap的初始化操作。

??

??

 

三 哈希計算

哈希表以哈希命名,足以說明哈希計算在該數(shù)據(jù)結(jié)構(gòu)中的重要程度。而在實現(xiàn)中,JDK并沒有直接使用Object的native方法返回的hashCode作為最終的哈希值,而是進行了二次加工。

以下分別為HashMap與ConcurrentHashMap計算hash值的方法,核心的計算邏輯相同,都是使用key對應(yīng)的hashCode與其hashCode右移16位的結(jié)果進行異或操作。此處,將高16位與低16位進行異或的操作稱之為擾動函數(shù),目的是將高位的特征融入到低位之中,降低哈希沖突的概率。

??

??

 

舉個例子來理解下擾動函數(shù)的作用:

hashCode(key1) = 0000 0000 0000 1111 0000 0000 0000 0010 
hashCode(key2) = 0000 0000 0000 0000 0000 0000 0000 0010

若HashMap容量為4,在不使用擾動函數(shù)的情況下,key1與key2的hashCode注定會沖突(后兩位相同,均為01)。

經(jīng)過擾動函數(shù)處理后,可見key1與key2 hashcode的后兩位不同,上述的哈希沖突也就避免了。

hashCode(key1) ^ (hashCode(key1) >>> 16) 
0000 0000 0000 1111 0000 0000 0000 1101

hashCode(key2) ^ (hashCode(key2) >>> 16)
0000 0000 0000 0000 0000 0000 0000 0010

這種增益會隨著HashMap容量的減少而增加?!禔n introduction to optimising a hashing strategy》文章中隨機選取了哈希值不同的352個字符串,當(dāng)HashMap的容量為2^9時,使用擾動函數(shù)可以減少10%的碰撞,可見擾動函數(shù)的必要性。

此外,ConcurrentHashMap中經(jīng)過擾亂函數(shù)處理之后,需要與HASH_BITS做與運算,HASH_BITS為0x7ffffff,即只有最高位為0,這樣運算的結(jié)果使hashCode永遠為正數(shù)。在ConcurrentHashMap中,預(yù)定義了幾個特殊節(jié)點的hashCode,如:MOVED、TREEBIN、RESERVED,它們的hashCode均定義為負(fù)值。因此,將普通節(jié)點的hashCode限定為正數(shù),也就是為了防止與這些特殊節(jié)點的hashCode產(chǎn)生沖突。

1 哈希沖突

通過哈希運算,可以將不同的輸入值映射到指定的區(qū)間范圍內(nèi),隨之而來的是哈希沖突問題??紤]一個極端的case,假設(shè)所有的輸入元素經(jīng)過哈希運算之后,都映射到同一個哈希桶中,那么查詢的復(fù)雜度將不再是O(1),而是O(n),相當(dāng)于線性表的順序遍歷。因此,哈希沖突是影響哈希計算性能的重要因素之一。哈希沖突如何解決呢?主要從兩個方面考慮,一方面是避免沖突,另一方面是在沖突時合理地解決沖突,盡可能提高查詢效率。前者在上面的章節(jié)中已經(jīng)進行介紹,即通過擾動函數(shù)來增加hashCode的隨機性,避免沖突。針對后者,HashMap中給出了兩種方案:拉鏈表與紅黑樹。

拉鏈表

在JDK1.8之前,HashMap中是采用拉鏈表的方法來解決沖突,即當(dāng)計算出的hashCode對應(yīng)的桶上已經(jīng)存在元素,但兩者key不同時,會基于桶中已存在的元素拉出一條鏈表,將新元素鏈到已存在元素的前面。當(dāng)查詢存在沖突的哈希桶時,會順序遍歷沖突鏈上的元素。同一key的判斷邏輯如下圖所示,先判斷hash值是否相同,再比較key的地址或值是否相同。

??

??

 

(1)死鏈

在JDK1.8之前,HashMap在并發(fā)場景下擴容時存在一個bug,形成死鏈,導(dǎo)致get該位置元素的時候,會死循環(huán),使CPU利用率高居不下。這也說明了HashMap不適于用在高并發(fā)的場景,高并發(fā)應(yīng)該優(yōu)先考慮JUC中的ConcurrentHashMap。然而,精益求精的JDK開發(fā)者們并沒有選擇繞過問題,而是選擇直面問題并解決它。在JDK1.8之中,引入了高低位鏈表(雙端鏈表)。

什么是高低位鏈表呢?在擴容時,哈希桶數(shù)組buckets會擴容一倍,以容量為8的HashMap為例,原有容量8擴容至16,將[0, 7]稱為低位,[8, 15]稱為高位,低位對應(yīng)loHead、loTail,高位對應(yīng)hiHead、hiTail。

擴容時會依次遍歷舊buckets數(shù)組的每一個位置上面的元素:

  • 若不存在沖突,則重新進行hash取模,并copy到新buckets數(shù)組中的對應(yīng)位置。
  • 若存在沖突元素,則采用高低位鏈表進行處理。通過e.hash & oldCap來判斷取模后是落在高位還是低位。舉個例子:假設(shè)當(dāng)前元素hashCode為0001(忽略高位),其運算結(jié)果等于0,說明擴容后結(jié)果不變,取模后還是落在低位[0, 7],即0001 & 1000 = 0000,還是原位置,再用低位鏈表將這類的元素鏈接起來。假設(shè)當(dāng)前元素的hashCode為1001, 其運算結(jié)果不為0,即1001 & 1000 = 1000 ,擴容后會落在高位,新的位置剛好是舊數(shù)組索引(1) + 舊數(shù)據(jù)長度(8) = 9,再用高位鏈表將這些元素鏈接起來。最后,將高低位鏈表的頭節(jié)點分別放在擴容后數(shù)組newTab的指定位置上,即完成了擴容操作。這種實現(xiàn)降低了對共享資源newTab的訪問頻次,先組織沖突節(jié)點,最后再放入newTab的指定位置。避免了JDK1.8之前每遍歷一個元素就放入newTab中,從而導(dǎo)致并發(fā)擴容下的死鏈問題。

??

??

 

紅黑樹

在JDK1.8之中,HashMap引入了紅黑樹來處理哈希沖突問題,而不再是拉鏈表。那么為什么要引入紅黑樹來替代鏈表呢?雖然鏈表的插入性能是O(1),但查詢性能確是O(n),當(dāng)哈希沖突元素非常多時,這種查詢性能是難以接受的。因此,在JDK1.8中,如果沖突鏈上的元素數(shù)量大于8,并且哈希桶數(shù)組的長度大于64時,會使用紅黑樹代替鏈表來解決哈希沖突,此時的節(jié)點會被封裝成TreeNode而不再是Node(TreeNode其實繼承了Node,以利用多態(tài)特性),使查詢具備O(logn)的性能。

這里簡單地回顧一下紅黑樹,它是一種平衡的二叉樹搜索樹,類似地還有AVL樹。兩者核心的區(qū)別是AVL樹追求“絕對平衡”,在插入、刪除節(jié)點時,成本要高于紅黑樹,但也因此擁有了更好的查詢性能,適用于讀多寫少的場景。然而,對于HashMap而言,讀寫操作其實難分伯仲,因此選擇紅黑樹也算是在讀寫性能上的一種折中。

四 位運算

1 確定哈希桶數(shù)組大小

找到大于等于給定值的最小2的整數(shù)次冪。

tableSizeFor根據(jù)輸入容量大小cap來計算最終哈希桶數(shù)組的容量大小,找到大于等于給定值cap的最小2的整數(shù)次冪。乍眼一看,這一行一行的位運算讓人云里霧里,莫不如采用類似找規(guī)律的方式來探索其中的奧秘。

??

??

 

當(dāng)cap為3時,計算過程如下:

cap = 3 
n = 2
n |= n >>> 1 010 | 001 = 011 n = 3
n |= n >>> 2 011 | 000 = 011 n = 3
n |= n >>> 4 011 | 000 = 011 n = 3
….
n = n + 1 = 4

當(dāng)cap為5時,計算過程如下:

cap = 5 
n = 4
n |= n >>> 1 0100 | 0010 = 0110 n = 6
n |= n >>> 2 0110 | 0001 = 0111 n = 7
….
n = n + 1 = 8

因此,計算的意義在于找到大于等于cap的最小2的整數(shù)次冪。整個過程是找到cap對應(yīng)二進制中最高位的1,然后每次以2倍的步長(依次移位1、2、4、8、16)復(fù)制最高位1到后面的所有低位,把最高位1后面的所有位全部置為1,最后進行+1,即完成了進位。

類似二進制位的變化過程如下:

0100 1010 
0111 1111
1000 0000

找到輸入cap的最小2的整數(shù)次冪作為最終容量可以理解為最小可用原則,盡可能地少占用空間,但是為什么必須要2的整數(shù)次冪呢?答案是,為了提高計算與存儲效率,使每個元素對應(yīng)hash值能夠準(zhǔn)確落入哈希桶數(shù)組給定的范圍區(qū)間內(nèi)。確定數(shù)組下標(biāo)采用的算法是 hash & (n - 1),n即為哈希桶數(shù)組的大小。由于其總是2的整數(shù)次冪,這意味著n-1的二進制形式永遠都是0000111111的形式,即從最低位開始,連續(xù)出現(xiàn)多個1,該二進制與任何值進行&運算都會使該值映射到指定區(qū)間[0, n-1]。比如:當(dāng)n=8時,n-1對應(yīng)的二進制為0111,任何與0111進行&運算都會落入[0,7]的范圍內(nèi),即落入給定的8個哈希桶中,存儲空間利用率100%。舉個反例,當(dāng)n=7,n-1對應(yīng)的二進制為0110,任何與0110進行&運算會落入到第0、6、4、2個哈希桶,而不是[0,6]的區(qū)間范圍內(nèi),少了1、3、5三個哈希桶,這導(dǎo)致存儲空間利用率只有不到60%,同時也增加了哈希碰撞的幾率。

2 ASHIFT偏移量計算

獲取給定值的最高有效位數(shù)(移位除了能夠進行乘除運算,還能用于保留高、低位操作,右移保留高位,左移保留低位)。

ConcurrentHashMap中的ABASE+ASHIFT是用來計算哈希數(shù)組中某個元素在實際內(nèi)存中的初始位置,ASHIFT采取的計算方式是31與scale前導(dǎo)0的數(shù)量做差,也就是scale的實際位數(shù)-1。scale就是哈希桶數(shù)組Node[]中每個元素的大小,通過((long)i << ASHIFT) + ABASE)進行計算,便可得到數(shù)組中第i個元素的起始內(nèi)存地址。

??

??

 

我們繼續(xù)看下前導(dǎo)0的數(shù)量是怎么計算出來的,numberOfLeadingZeros是Integer的靜態(tài)方法,還是沿用找規(guī)律的方式一探究竟。

??

??

 

假設(shè) i = 0000 0000 0000 0100 0000 0000 0000 0000,n = 1

i >>> 16  0000 0000 0000 0000 0000 0000 0000 0100   不為0 

i >>> 24 0000 0000 0000 0000 0000 0000 0000 0000 等于0

右移了24位等于0,說明24位到31位之間肯定全為0,即n = 1 + 8 = 9,由于高8位全為0,并且已經(jīng)將信息記錄至n中,因此可以舍棄高8位,即 i <<= 8。此時,

i = 0000 0100 0000 0000 0000 0000 0000 0000

類似地,i >>> 28 也等于0,說明28位到31位全為0,n = 9 + 4 = 13,舍棄高4位。此時,

i = 0100 0000 0000 0000 0000 0000 0000 0000

繼續(xù)運算,

i >>> 30  0000 0000 0000 0000 0000 0000 0000 0001   不為0 
i >>> 31 0000 0000 0000 0000 0000 0000 0000 0000 等于0

最終可得出n = 13,即有13個前導(dǎo)0。n -= i >>> 31是檢查最高位31位是否是1,因為n初始化為1,如果最高位是1,則不存在前置0,即n = n - 1 = 0。

總結(jié)一下,以上的操作其實是基于二分法的思想來定位二進制中1的最高位,先看高16位,若為0,說明1存在于低16位;反之存在高16位。由此將搜索范圍由32位(確切的說是31位)減少至16位,進而再一分為二,校驗高8位與低8位,以此類推。

計算過程中校驗的位數(shù)依次為16、8、4、2、1,加起來剛好為31。為什么是31不是32呢?因為前置0的數(shù)量為32的情況下i只能為0,在前面的if條件中已經(jīng)進行過濾。這樣一來,非0值的情況下,前置0只能出現(xiàn)在高31位,因此只需要校驗高31位即可。最終,用總位數(shù)減去計算出來的前導(dǎo)0的數(shù)量,即可得出二進制的最高有效位數(shù)。代碼中使用的是31 - Integer.numberOfLeadingZeros(scale),而不是總位數(shù)32,這是為了能夠得到哈希桶數(shù)組中第i個元素的起始內(nèi)存地址,方便進行CAS等操作。

五 并發(fā)

1 悲觀鎖

全表鎖

HashTable中采用了全表鎖,即所有操作均上鎖,串行執(zhí)行,如下圖中的put方法所示,采用synchronized關(guān)鍵字修飾。這樣雖然保證了線程安全,但是在多核處理器時代也極大地影響了計算性能,這也致使HashTable逐漸淡出開發(fā)者們的視野。

??

??

 

分段鎖

針對HashTable中鎖粒度過粗的問題,在JDK1.8之前,ConcurrentHashMap引入了分段鎖機制。整體的存儲結(jié)構(gòu)如下圖所示,在原有結(jié)構(gòu)的基礎(chǔ)上拆分出多個segment,每個segment下再掛載原來的entry(上文中經(jīng)常提到的哈希桶數(shù)組),每次操作只需要鎖定元素所在的segment,不需要鎖定整個表。因此,鎖定的范圍更小,并發(fā)度也會得到提升。

??

??

 

2 樂觀鎖

Synchronized+CAS

雖然引入了分段鎖的機制,即可以保證線程安全,又可以解決鎖粒度過粗導(dǎo)致的性能低下問題,但是對于追求極致性能的工程師來說,這還不是性能的天花板。因此,在JDK1.8中,ConcurrentHashMap摒棄了分段鎖,使用了樂觀鎖的實現(xiàn)方式。放棄分段鎖的原因主要有以下幾點:

  • 使用segment之后,會增加ConcurrentHashMap的存儲空間。
  • 當(dāng)單個segment過大時,并發(fā)性能會急劇下降。

ConcurrentHashMap在JDK1.8中的實現(xiàn)廢棄了之前的segment結(jié)構(gòu),沿用了與HashMap中的類似的Node數(shù)組結(jié)構(gòu)。

??

??

 

ConcurrentHashMap中的樂觀鎖是采用synchronized+CAS進行實現(xiàn)的。這里主要看下put的相關(guān)代碼。

當(dāng)put的元素在哈希桶數(shù)組中不存在時,則直接CAS進行寫操作。

??

??

 

這里涉及到了兩個重要的操作,tabAt與casTabAt。可以看出,這里面都使用了Unsafe類的方法。Unsafe這個類在日常的開發(fā)過程中比較罕見。我們通常對Java語言的認(rèn)知是:Java語言是安全的,所有操作都基于JVM,在安全可控的范圍內(nèi)進行。然而,Unsafe這個類會打破這個邊界,使Java擁有C的能力,可以操作任意內(nèi)存地址,是一把雙刃劍。這里使用到了前文中所提到的ASHIFT,來計算出指定元素的起始內(nèi)存地址,再通過getObjectVolatile與compareAndSwapObject分別進行取值與CAS操作。

在獲取哈希桶數(shù)組中指定位置的元素時為什么不能直接get而是要使用getObjectVolatile呢?因為在JVM的內(nèi)存模型中,每個線程有自己的工作內(nèi)存,也就是棧中的局部變量表,它是主存的一份copy。因此,線程1對某個共享資源進行了更新操作,并寫入到主存,而線程2的工作內(nèi)存之中可能還是舊值,臟數(shù)據(jù)便產(chǎn)生了。Java中的volatile是用來解決上述問題,保證可見性,任意線程對volatile關(guān)鍵字修飾的變量進行更新時,會使其它線程中該變量的副本失效,需要從主存中獲取最新值。雖然ConcurrentHashMap中的Node數(shù)組是由volatile修飾的,可以保證可見性,但是Node數(shù)組中元素是不具備可見性的。因此,在獲取數(shù)據(jù)時通過Unsafe的方法直接到主存中拿,保證獲取的數(shù)據(jù)是最新的。

??

??

 

繼續(xù)往下看put方法的邏輯,當(dāng)put的元素在哈希桶數(shù)組中存在,并且不處于擴容狀態(tài)時,則使用synchronized鎖定哈希桶數(shù)組中第i個位置中的第一個元素f(頭節(jié)點2),接著進行double check,類似于DCL單例模式的思想。校驗通過后,會遍歷當(dāng)前沖突鏈上的元素,并選擇合適的位置進行put操作。此外,ConcurrentHashMap也沿用了HashMap中解決哈希沖突的方案,鏈表+紅黑樹。這里只有在發(fā)生哈希沖突的情況下才使用synchronized鎖定頭節(jié)點,其實是比分段鎖更細(xì)粒度的鎖實現(xiàn),只在特定場景下鎖定其中一個哈希桶,降低鎖的影響范圍。

??

??

 

Java Map針對并發(fā)場景解決方案的演進方向可以歸結(jié)為,從悲觀鎖到樂觀鎖,從粗粒度鎖到細(xì)粒度鎖,這也可以作為我們在日常并發(fā)編程中的指導(dǎo)方針。

3 并發(fā)求和

CounterCell是JDK1.8中引入用來并發(fā)求和的利器,而在這之前采用的是【嘗試無鎖求和】+【沖突時加鎖重試】的策略??聪翪ounterCell的注釋,它是改編自LongAdder和Striped64。我們先看下求和操作,其實就是取baseCount作為初始值,然后遍歷CounterCell數(shù)組中的每一個cell,將各個cell的值進行累加。這里額外說明下@sun.misc.Contender注解的作用,它是Java8中引入用來解決緩存行偽共享問題的。什么是偽共享呢?簡單說下,考慮到CPU與主存之間速度的巨大差異,在CPU中引入了L1、L2、L3多級緩存,緩存中的存儲單位是緩存行,緩存行大小為2的整數(shù)次冪字節(jié),32-256個字節(jié)不等,最常見的是64字節(jié)。因此,這將導(dǎo)致不足64字節(jié)的變量會共享同一個緩存行,其中一個變量失效會影響到同一個緩存行中的其他變量,致使性能下降,這就是偽共享問題??紤]到不同CPU的緩存行單位的差異性,Java8中便通過該注解將這種差異性屏蔽,根據(jù)實際緩存行大小來進行填充,使被修飾的變量能夠獨占一個緩存行。

??

??

??

??

 

再來看下CounterCell是如何實現(xiàn)計數(shù)的,每當(dāng)map中的容量有變化時會調(diào)用addCount進行計數(shù),核心邏輯如下:

  • 當(dāng)counterCells不為空,或counterCells為空且對baseCount進行CAS操作失敗時進入到后續(xù)計數(shù)處理邏輯,否則對baseCount進行CAS操作成功,直接返回。
  • 后續(xù)計數(shù)處理邏輯中會調(diào)用核心計數(shù)方法fullAddCount,但需要滿足以下4個條件中的任意一個:1、counterCells為空;2、counterCells的size為0;3、counterCells對應(yīng)位置上的counterCell為空;4、CAS更新counterCells對應(yīng)位置上的counterCell失敗。這些條件背后的語義是,當(dāng)前情況下,計數(shù)已經(jīng)或曾經(jīng)出現(xiàn)過并發(fā)沖突,需要優(yōu)先借助于CounterCell來解決。若counterCells與對應(yīng)位置上的元素已經(jīng)初始化(條件4),則先嘗試CAS進行更新,若失敗則調(diào)用fullAddCount繼續(xù)處理。若counterCells與對應(yīng)位置上的元素未初始化完成(條件1、2、3),也要調(diào)用AddCount進行后續(xù)處理。
  • 這里確定cell下標(biāo)時采用了ThreadLocalRandom.getProbe()作為哈希值,這個方法返回的是當(dāng)前Thread中threadLocalRandomProbe字段的值。而且當(dāng)哈希值沖突時,還可以通過advanceProbe方法來更換哈希值。這與HashMap中的哈希值計算邏輯不同,因為HashMap中要保證同一個key進行多次哈希計算的哈希值相同并且能定位到對應(yīng)的value,即便兩個key的哈希值沖突也不能隨便更換哈希值,只能采用鏈表或紅黑樹處理沖突。然而在計數(shù)場景,我們并不需要維護key-value的關(guān)系,只需要在counterCells中找到一個合適的位置放入計數(shù)cell,位置的差異對最終的求和結(jié)果是沒有影響的,因此當(dāng)沖突時可以基于隨機策略更換一個哈希值來避免沖突。

??

??

 

接著,我們來看下核心計算邏輯fullAddCount,代碼還是比較多的,核心流程是通過一個死循環(huán)來實現(xiàn)的,循環(huán)體中包含了3個處理分支,為了方便講解我將它們依次定義A、B、C。

  • A:表示counterCells已經(jīng)初始化完成,因此可以嘗試更新或創(chuàng)建對應(yīng)位置的CounterCell。
  • B:表示counterCells未初始化完成,且無沖突(拿到cellsBusy鎖),則加鎖初始化counterCells,初始容量為2。
  • C:表示counterCells未初始化完成,且有沖突(未能拿到cellsBusy鎖),則CAS更新baseCount,baseCount在求和時也會被算入到最終結(jié)果中,這也相當(dāng)于是一種兜底策略,既然counterCells正在被其他線程鎖定,那當(dāng)前線程也沒必要再等待了,直接嘗試使用baseCount進行累加。

其中,A分支中涉及到的操作又可以拆分為以下幾點:

  • a1:對應(yīng)位置的CounterCell未創(chuàng)建,采用鎖+Double Check的策略嘗試創(chuàng)建CounterCell,失敗的話則continue進行重試。這里面采用的鎖是cellsBusy,它保證創(chuàng)建CounterCell并放入counterCells時一定是串行執(zhí)行,避免重復(fù)創(chuàng)建,其實就是使用了DCL單例模式的策略。在CounterCells的創(chuàng)建、擴容中都需要使用該鎖。
  • a2:沖突檢測,變量wasUncontended是調(diào)用方addCount中傳入的,表示前置的CAS更新cell失敗,有沖突,需要更換哈希值【a7】后繼續(xù)重試。
  • a3:對應(yīng)位置的CounterCell不為空,直接CAS進行更新。
  • a4:
  • 沖突檢測,當(dāng)counterCells的引用值不等于當(dāng)前線程對應(yīng)的引用值時,說明有其他線程更改了counterCells的引用,出現(xiàn)沖突,則將collide設(shè)為false,下次迭代時可進行擴容。
  • 容量限制,counterCells容量的最大值為大于等于NCPU(實際機器CPU核心的數(shù)量)的最小2的整數(shù)次冪,當(dāng)達到容量限制時后面的擴容分支便永遠不會執(zhí)行。這里限制的意義在于,真實并發(fā)度是由CPU核心來決定,當(dāng)counterCells容量與CPU核心數(shù)量相等時,理想情況下就算所有CPU核心在同時運行不同的計數(shù)線程時,都不應(yīng)該出現(xiàn)沖突,每個線程選擇各自的cell進行處理即可。如果出現(xiàn)沖突,一定是哈希值的問題,因此采取的措施是重新計算哈希值a7,而不是通過擴容來解決。時間換空間,避免不必要的存儲空間浪費,非常贊的想法~
  • a5:更新擴容標(biāo)志位,下次迭代時將會進行擴容。
  • a6:進行加鎖擴容,每次擴容1倍。
  • a7:更換哈希值。
private final void fullAddCount(long x, boolean wasUncontended) { 
int h;
// 初始化probe
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
// 用來控制擴容操作
boolean collide = false; // True if last slot nonempty
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
// 【A】counterCells已經(jīng)初始化完畢
if ((as = counterCells) != null && (n = as.length) > 0) {
// 【a1】對應(yīng)位置的CounterCell未創(chuàng)建
if ((a = as[(n - 1) & h]) == null) {
// cellsBusy其實是一個鎖,cellsBusy=0時表示無沖突
if (cellsBusy == 0) { // Try to attach new Cell
// 創(chuàng)建新的CounterCell
CounterCell r = new CounterCell(x); // Optimistic create
// Double Check,加鎖(通過CAS將cellsBusy設(shè)置1)
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean created = false;
try { // Recheck under lock
CounterCell[] rs; int m, j;
// Double Check
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
// 將新創(chuàng)建的CounterCell放入counterCells中
rs[j] = r;
created = true;
}
} finally {
// 解鎖,這里為什么不用CAS?因為當(dāng)前流程中需要在獲取鎖的前提下進行,即串行執(zhí)行,因此不存在并發(fā)更新問題,只需要正常更新即可
cellsBusy = 0;
}
if (created)
break;
// 創(chuàng)建失敗則重試
continue; // Slot is now non-empty
}
}
// cellsBusy不為0,說明被其他線程爭搶到了鎖,還不能考慮擴容
collide = false;
}
//【a2】沖突檢測
else if (!wasUncontended) // CAS already known to fail
// 調(diào)用方addCount中CAS更新cell失敗,有沖突,則繼續(xù)嘗試CAS
wasUncontended = true; // Continue after rehash

//【a3】對應(yīng)位置的CounterCell不為空,直接CAS進行更新
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
//【a4】容量限制
else if (counterCells != as || n >= NCPU)
// 說明counterCells容量的最大值為大于NCPU(實際機器CPU核心的數(shù)量)最小2的整數(shù)次冪。
// 這里限制的意義在于,并發(fā)度是由CPU核心來決定,當(dāng)counterCells容量與CPU核心數(shù)量相等時,理論上講就算所有CPU核心都在同時運行不同的計數(shù)線程時,都不應(yīng)該出現(xiàn)沖突,每個線程選擇各自的cell進行處理即可。如果出現(xiàn)沖突,一定是哈希值的問題,因此采取的措施是重新計算哈希值(h = ThreadLocalRandom.advanceProbe(h)),而不是通過擴容來解決

// 當(dāng)n大于NCPU時后面的分支就不會走到了
collide = false; // At max size or stale
// 【a5】更新擴容標(biāo)志位
else if (!collide)
// 說明映射到cell位置不為空,并且嘗試進行CAS更新時失敗了,則說明有競爭,將collide設(shè)置為true,下次迭代時執(zhí)行后面的擴容操作,降低競爭度
// 有競爭時,執(zhí)行rehash+擴容,當(dāng)容量大于CPU核心時則停止擴容只進行rehash
collide = true;
// 【a6】加鎖擴容
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
// 加鎖擴容
try {
if (counterCells == as) {// Expand table unless stale
// 擴容1倍
CounterCell[] rs = new CounterCell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
counterCells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
//【a7】更換哈希值
h = ThreadLocalRandom.advanceProbe(h);
}
// 【B】counterCells未初始化完成,且無沖突,則加鎖初始化counterCells
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try { // Initialize table
if (counterCells == as) {
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
// 【C】counterCells未初始化完成,且有沖突,則CAS更新baseCount
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}

CounterCell的設(shè)計很巧妙,它的背后其實就是JDK1.8中的LongAdder。核心思想是:在并發(fā)較低的場景下直接采用baseCount累加,否則結(jié)合counterCells,將不同的線程散列到不同的cell中進行計算,盡可能地確保訪問資源的隔離,減少沖突。LongAdder相比較于AtomicLong中無腦CAS的策略,在高并發(fā)的場景下,能夠減少CAS重試的次數(shù),提高計算效率。

六 結(jié)語

以上可能只是Java Map源碼中的冰山一角,但是基本包括了大部分的核心特性,涵蓋了我們?nèi)粘i_發(fā)中的大部分場景。讀源碼跟讀書一樣,仿佛跨越了歷史長河與作者進行近距離對話,揣摩他的心思,學(xué)習(xí)他的思想并加以傳承。信息加工轉(zhuǎn)化為知識并運用的過程是痛苦的,但是痛并快樂著。

 

責(zé)任編輯:武曉燕 來源: 51CTO專欄
相關(guān)推薦

2021-05-08 10:36:31

開發(fā)Java Map

2013-11-11 15:15:38

設(shè)計用戶體驗

2012-09-10 10:59:49

網(wǎng)頁設(shè)計jQueryCSS

2017-06-27 14:48:51

開發(fā)設(shè)計程序員

2022-12-12 08:35:51

Map容器接口

2022-07-10 07:48:26

緩存軟件設(shè)計

2021-06-25 10:18:08

JavaScript Array.map 巧技拾遺

2011-12-19 14:28:14

Java設(shè)計模式

2012-03-02 13:53:15

水果忍者手機游戲UI

2024-06-20 13:11:26

設(shè)計模式開發(fā)

2023-02-24 14:52:20

Redis存儲開發(fā)

2017-08-04 17:07:32

JavaArraysList

2009-06-25 15:20:28

CollectionMap

2021-05-21 17:47:01

奧卡云智能存儲

2010-10-13 10:16:44

備份VMware vS虛擬機

2012-07-10 15:55:55

移動App應(yīng)用設(shè)計

2013-10-11 09:58:30

系統(tǒng)設(shè)計

2018-08-19 13:27:21

數(shù)據(jù)庫緩存數(shù)據(jù)庫減負(fù)

2023-08-23 15:57:41

開發(fā)工具Java

2012-03-12 13:55:22

交互設(shè)計
點贊
收藏

51CTO技術(shù)棧公眾號

男人的天堂免费| 韩国黄色一级大片| 中文字幕第315页| 久久久久久久久丰满| 精品播放一区二区| 亚洲成色www.777999| 在线电影福利片| 久久午夜国产精品| 亚洲自拍偷拍一区| 欧美日韩一级黄色片| 欧美福利影院| 在线播放国产一区中文字幕剧情欧美 | 久久久免费观看视频| 久久亚洲AV无码专区成人国产| 亚洲伦理一区二区| 天天综合网 天天综合色| 伊人久久大香线蕉午夜av| 四虎永久在线精品免费网址| 久久激情综合网| 国产91精品久久久| 欧美精品入口蜜桃| 欧美色网址大全| 亚洲国产精品人人爽夜夜爽| 久久久久久久高清| 亚洲精品555| 欧美日韩在线另类| 99久久免费观看| 久cao在线| 国产日产欧美一区| 国产在线精品一区二区三区》 | 欧美色视频一区二区三区在线观看| 精品亚洲免a| 欧美一区二区久久| 中文字幕av专区| 欧美精品日日操| 狠狠综合久久av一区二区小说| 成人小视频在线观看免费| 3d成人动漫在线| 国产亚洲欧美日韩在线一区| 精品在线不卡| 午夜成人免费影院| av亚洲产国偷v产偷v自拍| 99在线国产| 国产av无码专区亚洲av麻豆| 久久99深爱久久99精品| 国产精品中文字幕在线| 香蕉污视频在线观看| 亚洲综合国产激情另类一区| 91国产精品91| 日韩免费视频一区二区视频在线观看| 欧美日韩精选| 久久久久国产精品免费网站| 青草影院在线观看| 911久久香蕉国产线看观看| 精品国模在线视频| 黄色一级大片在线免费观看| 一区二区影视| 九九视频直播综合网| 欧美极品视频在线观看| 国模一区二区三区| 国内成人精品视频| www.国产成人| 久久精品首页| 国产精品99久久久久久久久久久久 | 成人情趣视频| 日韩中文在线中文网三级| 刘亦菲国产毛片bd| 欧美一区二区三区另类| 欧美高清自拍一区| 国产尤物在线视频| 日韩精品一级中文字幕精品视频免费观看| 日本三级韩国三级久久| 中文字幕一区二区三区四区视频 | 91在线中文字幕| 国产ts变态重口人妖hd| 成人性色生活片免费看爆迷你毛片| 国产女主播一区二区| 日本啊v在线| 国产目拍亚洲精品99久久精品| 一区二区三区欧美成人| 在线观看h网| 欧美日韩在线一区| 国产又大又黄又粗又爽| 网站一区二区| 亚洲欧美激情另类校园| 特黄一区二区三区| 好看的日韩av电影| 日本精品一区二区三区在线播放视频| 中文字幕激情视频| 国产电影一区在线| 欧洲成人一区二区| gogo在线观看| 欧美日韩美女在线| 91看片破解版| 女人抽搐喷水高潮国产精品| 自拍视频国产精品| 日韩激情在线播放| 免费观看日韩电影| 国产精品三区www17con| 日本大片在线观看| 一区二区三区四区av| 三级4级全黄60分钟| 国产日韩在线观看视频| 亚洲美女精品成人在线视频| 欧美丰满熟妇bbbbbb| 久久青草久久| 国产精品久久久一区二区三区| 国产三级在线免费观看| 亚洲v精品v日韩v欧美v专区| 午夜免费看毛片| 五月国产精品| 色综合天天狠天天透天天伊人| 成人a v视频| 成人91在线观看| 中文字幕黄色大片| 性高爱久久久久久久久| 欧美精品一区二区高清在线观看| 日韩av片在线免费观看| 亚洲影院一区| 国产精品免费一区二区三区四区 | 1024成人网| 精品久久久久av| 欧美久久香蕉| 欧美高清无遮挡| av中文字幕免费| 中文字幕在线观看一区| 免费看黄色一级大片| 日韩电影在线观看完整免费观看| 欧美成年人视频网站| 在线免费看av的网站| 久久久久久久久久久99999| 大陆av在线播放| 欧美黄色一级| 久久在线精品视频| 一区二区三区免费观看视频| 国产亚洲一区字幕| 成人免费观看视频在线观看| swag国产精品一区二区| 美女少妇精品视频| 国产99对白在线播放| 亚洲欧洲日韩在线| 依人在线免费视频| 天天射成人网| 亚洲一区二区免费| 中国av在线播放| 欧美一区二区观看视频| 日本高清一二三区| 国产精品系列在线播放| 欧美 日韩 国产精品| 久久国际精品| 欧美日韩福利电影| 人妻精品无码一区二区| 亚洲电影第三页| 中国极品少妇xxxx| 亚洲美女少妇无套啪啪呻吟| 精品国产一区二区三区麻豆小说 | 国产福利在线视频| 欧洲国内综合视频| 999福利视频| 国产一区视频网站| 久久99久久99精品| 天天久久夜夜| 国产精品麻豆va在线播放| 在线免费观看黄色av| 欧美精品一二三四| 国产一区二区播放| 不卡的av电影| 成人一区二区三| 日本在线电影一区二区三区| 91免费精品国偷自产在线| 手机av免费在线| 亚洲国产高清高潮精品美女| 男人天堂视频网| 亚洲视频资源在线| 国产亚洲色婷婷久久99精品91| 亚洲欧美大片| 中国人体摄影一区二区三区| 91成人精品在线| 日韩美女激情视频| 国内外激情在线| 亚洲精品www| 亚洲图片视频小说| 亚洲一区二区三区自拍| 一本色道综合久久欧美日韩精品| 日韩av一级片| 大胆欧美熟妇xx| 欧美猛男同性videos| 91免费国产视频| 性孕妇free特大另类| 中文字幕精品网| 婷婷五月综合激情| 欧美精品1区2区3区| xxxx.国产| 亚洲天堂2014| 中文字幕国产综合| 国产白丝精品91爽爽久久 | 免费观看一级欧美片| 中文字幕亚洲第一| 天天干天天摸天天操| 精品视频一区 二区 三区| 久久精品波多野结衣| 欧美激情一区二区在线| 国模无码视频一区| 激情成人综合网| 久草精品在线播放| 欧美日韩第一区| 亚洲看片网站| 群体交乱之放荡娇妻一区二区 | 99在线精品视频免费观看软件| 高跟丝袜欧美一区| 青青草国产在线观看| 国产精品色噜噜| v8888av| 丰满放荡岳乱妇91ww| 少妇一级淫免费播放| 久久国产精品99国产| 欧美性猛交内射兽交老熟妇| 日韩理论在线| 日韩欧美精品在线不卡| 欧美男人操女人视频| 99伊人久久| 韩国三级成人在线| 国产精品视频色| 午夜日韩成人影院| 69av在线播放| av手机在线观看| 欧美国产日韩中文字幕在线| 免费日本一区二区三区视频| 在线视频欧美日韩| 国产在线电影| 亚洲天堂男人天堂| 欧美日韩国产中文字幕在线| 亚洲精品国产免费| 亚洲AV无码乱码国产精品牛牛| 91麻豆精品国产91久久久 | 真实国产乱子伦对白在线| 中文字幕欧美区| 欧美人妻一区二区三区| 久久精品夜色噜噜亚洲a∨| 一女三黑人理论片在线 | 一级黄色性视频| xfplay精品久久| 美女洗澡无遮挡| 国产日本亚洲高清| 国产传媒国产传媒| 中文在线一区二区| 日韩一区二区三区四区视频| 国产精品福利影院| frxxee中国xxx麻豆hd| 亚洲欧美在线高清| 少妇高潮在线观看| 亚洲蜜臀av乱码久久精品蜜桃| 国产日韩欧美在线观看视频| 伊人色综合久久天天人手人婷| 国产大学生自拍| 一二三四社区欧美黄| 久久精品一区二区三| 亚洲成人资源在线| 99热在线观看免费精品| 色综合视频在线观看| 一级特黄免费视频| 欧美电影在线免费观看| 国产999久久久| 亚洲国产精品va在看黑人| 四虎影视在线观看2413| 亚洲天堂影视av| 麻豆网站在线看| 色与欲影视天天看综合网| 99色在线观看| 欧美亚洲国产精品| 国产精品99| 国产精品久久一区二区三区| 性人久久久久| 一本一本a久久| 国产中文一区| 熟妇人妻无乱码中文字幕真矢织江| 九色porny丨国产精品| 欧洲熟妇的性久久久久久| 久久精品一级爱片| 欧美成人三级视频| 日韩欧美中文字幕在线播放| 日韩久久久久久久久久| 日韩欧美中文字幕一区| 涩爱av在线播放一区二区| 综合国产在线视频| 福利在线导航136| 国产精品成熟老女人| 亚洲三区欧美一区国产二区| 鲁鲁视频www一区二区| 欧美激情777| 日韩av高清在线看片| 男女男精品视频网| 日本黄色动态图| 国产精品二三区| 毛片在线免费视频| 欧美一区永久视频免费观看| 理论视频在线| 欧美激情视频网站| 国产成人a视频高清在线观看| 成人性生交大片免费看视频直播| 成人看片爽爽爽| 一区二区三区三区在线| 在线亚洲精品| 亚洲高清视频免费| 中文字幕免费观看一区| 日本一级淫片免费放| 欧美精三区欧美精三区| 久久久资源网| 久久久免费观看视频| 成人午夜888| 亚洲一区二区不卡视频| 亚洲欧美日韩国产一区二区| 亚洲午夜精品在线观看| 国产精品久久影院| 无码人妻一区二区三区免费| 日韩精品一区二| 日本最新在线视频| 国产成人精品久久二区二区91| 99久久香蕉| 日本在线视频www色| 日韩精品高清不卡| 五月婷婷综合在线观看| 亚洲一区二区av在线| 国产成人三级在线播放| 久久精品久久久久久| 成人黄色图片网站| 欧美污视频久久久| 亚洲一区二区三区免费在线观看| av在线天堂网| 亚洲蜜桃精久久久久久久| 国产又粗又猛又爽又黄的视频一| 在线精品播放av| 日韩高清中文字幕一区二区| 欧美福利精品| 久久男女视频| www.狠狠爱| 色999日韩国产欧美一区二区| 五月激情婷婷综合| 午夜剧场成人观在线视频免费观看| 日韩免费高清视频网站| 青青在线免费视频| 国产麻豆精品theporn| 污污的视频在线免费观看| 欧美电影一区二区| h片在线免费| av一区二区三区四区电影| 国产精品啊啊啊| 制服丝袜在线第一页| 亚洲成人中文在线| 日本中文字幕电影在线观看| 欧美一级电影久久| 一区二区三区日本久久久| 欧美一级在线看| 久久久久国产精品厨房| 91麻豆精品在线| 色偷偷噜噜噜亚洲男人的天堂| 999国产精品亚洲77777| 亚洲精品一区二区三| 韩国三级在线一区| 久久久久人妻一区精品色欧美| 精品久久久久久久久久久久久久久久久| www在线免费观看视频| av一区二区在线看| 亚洲欧美激情诱惑| 国产欧美一区二区三区在线观看视频| 欧美性xxxxxx少妇| 日本三级视频在线观看| 亚洲淫片在线视频| 日韩香蕉视频| av网站免费在线播放| 欧美三级中文字幕| www视频在线免费观看| 国产日韩亚洲精品| 久久久久99| 亚洲图片第一页| 日韩美女天天操| 成人免费看黄| 在线观看欧美激情| 成人免费高清在线观看| 久久久久久久久久成人| 久久精品99久久久久久久久 | 麻豆国产精品视频| 青娱乐在线视频免费观看| 亚洲第一福利在线观看| 午夜无码国产理论在线| 97精品国产97久久久久久粉红 | 偷拍夫妻性生活| 欧美卡1卡2卡| 国产在线精彩视频| 亚洲午夜精品一区二区三区| 国产成人一区二区精品非洲| 国产精品va无码一区二区三区| 日韩有码在线播放| 日韩av字幕| 国产福利精品一区二区三区| 午夜精品一区二区三区三上悠亚| 成人精品福利| 国产一区二区三区四区hd| 麻豆成人综合网| 西西44rtwww国产精品|