Concurrenthashmap原理分析 有用

一器腋、背景:

線程不安全的HashMap

因為多線程環(huán)境下,使用Hashmap進(jìn)行put操作會引起死循環(huán)钩杰,導(dǎo)致CPU利用率接近100%纫塌,所以在并發(fā)情況下不能使用HashMap。

效率低下的HashTable容器

HashTable容器使用synchronized來保證線程安全讲弄,但在線程競爭激烈的情況下HashTable的效率非常低下护戳。因為當(dāng)一個線程訪問HashTable的同步方法時,其他線程訪問HashTable的同步方法時垂睬,可能會進(jìn)入阻塞或輪詢狀態(tài)媳荒。如線程1使用put進(jìn)行添加元素,線程2不但不能使用put方法添加元素驹饺,并且也不能使用get方法來獲取元素钳枕,所以競爭越激烈效率越低。

鎖分段技術(shù)

HashTable容器在競爭激烈的并發(fā)環(huán)境下表現(xiàn)出效率低下的原因赏壹,是因為所有訪問HashTable的線程都必須競爭同一把鎖鱼炒,那假如容器里有多把鎖,每一把鎖用于鎖容器其中一部分?jǐn)?shù)據(jù)蝌借,那么當(dāng)多線程訪問容器里不同數(shù)據(jù)段的數(shù)據(jù)時昔瞧,線程間就不會存在鎖競爭指蚁,從而可以有效的提高并發(fā)訪問效率,
ConcurrentHashMap所使用的鎖分段技術(shù)自晰,首先將數(shù)據(jù)分成一段一段的存儲凝化,然后給每一段數(shù)據(jù)配一把鎖,當(dāng)一個線程占用鎖訪問其中一個段數(shù)據(jù)的時候酬荞,其他段的數(shù)據(jù)也能被其他線程訪問搓劫。有些方法需要跨段,比如size()和containsValue()混巧,它們可能需要鎖定整個表而而不僅僅是某個段枪向,這需要按順序鎖定所有段,操作完畢后咧党,又按順序釋放所有段的鎖秘蛔。這里“按順序”是很重要的,否則極有可能出現(xiàn)死鎖傍衡,在ConcurrentHashMap內(nèi)部深员,段數(shù)組是final的,并且其成員變量實際上也是final的聪舒,但是辨液,僅僅是將數(shù)組聲明為final的并不保證數(shù)組成員也是final的,這需要實現(xiàn)上的保證箱残。這可以確保不會出現(xiàn)死鎖滔迈,因為獲得鎖的順序是固定的。

image.png

ConcurrentHashMap是由Segment數(shù)組結(jié)構(gòu)和HashEntry數(shù)組結(jié)構(gòu)組成被辑。Segment是一種可重入鎖ReentrantLock燎悍,在ConcurrentHashMap里扮演鎖的角色,HashEntry則用于存儲鍵值對數(shù)據(jù)盼理。一個ConcurrentHashMap里包含一個Segment數(shù)組谈山,Segment的結(jié)構(gòu)和HashMap類似,是一種數(shù)組和鏈表結(jié)構(gòu)宏怔, 一個Segment里包含一個HashEntry數(shù)組奏路,每個HashEntry是一個鏈表結(jié)構(gòu)的元素, 每個Segment守護(hù)者一個HashEntry數(shù)組里的元素,當(dāng)對HashEntry數(shù)組的數(shù)據(jù)進(jìn)行修改時臊诊,必須首先獲得它對應(yīng)的Segment鎖鸽粉。

二、應(yīng)用場景

當(dāng)有一個大數(shù)組時需要在多個線程共享時就可以考慮是否把它給分層多個節(jié)點了抓艳,避免大鎖触机。并可以考慮通過hash算法進(jìn)行一些模塊定位。
其實不止用于線程,當(dāng)設(shè)計數(shù)據(jù)表的事務(wù)時(事務(wù)某種意義上也是同步機(jī)制的體現(xiàn))儡首,可以把一個表看成一個需要同步的數(shù)組片任,如果操作的表數(shù)據(jù)太多時就可以考慮事務(wù)分離了(這也是為什么要避免大表的出現(xiàn)),比如把數(shù)據(jù)進(jìn)行字段拆分蔬胯,水平分表等.

三对供、源碼解讀

ConcurrentHashMap(1.7及之前)中主要實體類就是三個:ConcurrentHashMap(整個Hash表),Segment(桶),HashEntry(節(jié)點)笔宿,對應(yīng)上面的圖可以看出之間的關(guān)系

/** 
* The segments, each of which is a specialized hash table 
*/  
final Segment<K,V>[] segments;
不變(Immutable)和易變(Volatile)
ConcurrentHashMap完全允許多個讀操作并發(fā)進(jìn)行犁钟,讀操作并不需要加鎖棱诱。如果使用傳統(tǒng)的技術(shù)泼橘,如HashMap中的實現(xiàn),如果允許可以在hash鏈的中間添加或刪除元素迈勋,讀操作不加鎖將得到不一致的數(shù)據(jù)炬灭。ConcurrentHashMap實現(xiàn)技術(shù)是保證HashEntry幾乎是不可變的。HashEntry代表每個hash鏈中的一個節(jié)點靡菇,其結(jié)構(gòu)如下所示:
static final class HashEntry<K,V> {  
     final K key;  
     final int hash;  
     volatile V value;  
     final HashEntry<K,V> next;  
 } 

可以看到除了value不是final的重归,其它值都是final的,這意味著不能從hash鏈的中間或尾部添加或刪除節(jié)點厦凤,因為這需要修改next 引用值鼻吮,所有的節(jié)點的修改只能從頭部開始。對于put操作较鼓,可以一律添加到Hash鏈的頭部椎木。但是對于remove操作,可能需要從中間刪除一個節(jié)點博烂,這就需要將要刪除節(jié)點的前面所有節(jié)點整個復(fù)制一遍香椎,最后一個節(jié)點指向要刪除結(jié)點的下一個結(jié)點。這在講解刪除操作時還會詳述禽篱。為了確保讀操作能夠看到最新的值畜伐,將value設(shè)置成volatile,這避免了加鎖躺率。

其它

為了加快定位段以及段中hash槽的速度,每個段hash槽的的個數(shù)都是2^n慎框,這使得通過位運算就可以定位段和段中hash槽的位置。當(dāng)并發(fā)級別為默認(rèn)值16時,也就是段的個數(shù),hash值的高4位決定分配在哪個段中。但是我們也不要忘記《算法導(dǎo)論》給我們的教訓(xùn):hash槽的的個數(shù)不應(yīng)該是 2^n,這可能導(dǎo)致hash槽分配不均衡未,這需要對hash值重新再hash一次送粱。(這段似乎有點多余了 )

定位操作:

 final Segment<K,V> segmentFor(int hash) {  
     return segments[(hash >>> segmentShift) & segmentMask];  
 }

既然ConcurrentHashMap使用分段鎖Segment來保護(hù)不同段的數(shù)據(jù)橄镜,那么在插入和獲取元素的時候裆馒,必須先通過哈希算法定位到Segment梗搅∝ね鳎可以看到ConcurrentHashMap會首先使用Wang/Jenkins hash的變種算法對元素的hashCode進(jìn)行一次再哈希闪盔。
再哈希族淮,其目的是為了減少哈希沖突祝辣,使元素能夠均勻的分布在不同的Segment上,從而提高容器的存取效率切油。假如哈希的質(zhì)量差到極點蝙斜,那么所有的元素都在一個Segment中,不僅存取元素緩慢澎胡,分段鎖也會失去意義孕荠。我做了一個測試,不通過再哈希而直接執(zhí)行哈希計算攻谁。

System.out.println(Integer.parseInt("0001111", 2) & 15);
System.out.println(Integer.parseInt("0011111", 2) & 15);
System.out.println(Integer.parseInt("0111111", 2) & 15);
System.out.println(Integer.parseInt("1111111", 2) & 15);

計算后輸出的哈希值全是15稚伍,通過這個例子可以發(fā)現(xiàn)如果不進(jìn)行再哈希,哈希沖突會非常嚴(yán)重戚宦,因為只要低位一樣个曙,無論高位是什么數(shù),其哈希值總是一樣受楼。我們再把上面的二進(jìn)制數(shù)據(jù)進(jìn)行再哈希后結(jié)果如下垦搬,為了方便閱讀,不足32位的高位補(bǔ)了0艳汽,每隔四位用豎線分割下猴贰。

0100|0111|0110|0111|1101|1010|0100|1110
1111|0111|0100|0011|0000|0001|1011|1000
0111|0111|0110|1001|0100|0110|0011|1110
1000|0011|0000|0000|1100|1000|0001|1010

可以發(fā)現(xiàn)每一位的數(shù)據(jù)都散列開了,通過這種再哈希能讓數(shù)字的每一位都能參加到哈希運算當(dāng)中河狐,從而減少哈希沖突米绕。ConcurrentHashMap通過以下哈希算法定位segment瑟捣。
默認(rèn)情況下segmentShift為28,segmentMask為15栅干,再哈希后的數(shù)最大是32位二進(jìn)制數(shù)據(jù)蝶柿,向右無符號移動28位,意思是讓高4位參與到hash運算中非驮, (hash >>> segmentShift) & segmentMask的運算結(jié)果分別是4交汤,15,7和8劫笙,可以看到hash值沒有發(fā)生沖突芙扎。

final Segment<K,V> segmentFor(int hash) {
    return segments[(hash >>> segmentShift) & segmentMask];
}

數(shù)據(jù)結(jié)構(gòu)

所有的成員都是final的,其中segmentMask和segmentShift主要是為了定位段填大,參見上面的segmentFor方法戒洼。
關(guān)于Hash表的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu),這里不想做過多的探討允华。Hash表的一個很重要方面就是如何解決hash沖突圈浇,ConcurrentHashMap 和HashMap使用相同的方式,都是將hash值相同的節(jié)點放在一個hash鏈中靴寂。與HashMap不同的是磷蜀,ConcurrentHashMap使用多個子Hash表,也就是段(Segment)百炬。
每個Segment相當(dāng)于一個子Hash表褐隆,它的數(shù)據(jù)成員如下:

static final class Segment<K,V> extends ReentrantLock implements Serializable {    
         /** 
          * The number of elements in this segment's region. 
          */
         transient volatileint count;  
         /** 
          * Number of updates that alter the size of the table. This is 
          * used during bulk-read methods to make sure they see a 
          * consistent snapshot: If modCounts change during a traversal 
          * of segments computing size or checking containsValue, then 
          * we might have an inconsistent view of state so (usually) 
          * must retry. 
          */
         transient int modCount;  
         /** 
          * The table is rehashed when its size exceeds this threshold. 
          * (The value of this field is always <tt>(int)(capacity * 
          * loadFactor)</tt>.) 
          */
         transient int threshold;  
         /** 
          * The per-segment table. 
          */
         transient volatile HashEntry<K,V>[] table;  
         /** 
          * The load factor for the hash table.  Even though this value 
          * is same for all segments, it is replicated to avoid needing 
          * links to outer object. 
          * @serial 
          */
         final float loadFactor;  
 }

count用來統(tǒng)計該段數(shù)據(jù)的個數(shù),它是volatile剖踊,它用來協(xié)調(diào)修改和讀取操作庶弃,以保證讀取操作能夠讀取到幾乎最新的修改。協(xié)調(diào)方式是這樣的德澈,每次修改操作做了結(jié)構(gòu)上的改變歇攻,如增加/刪除節(jié)點(修改節(jié)點的值不算結(jié)構(gòu)上的改變),都要寫count值梆造,每次讀取操作開始都要讀取count的值缴守。這利用了 Java 5中對volatile語義的增強(qiáng),對同一個volatile變量的寫和讀存在happens-before關(guān)系澳窑。modCount統(tǒng)計段結(jié)構(gòu)改變的次數(shù)斧散,主要是為了檢測對多個段進(jìn)行遍歷過程中某個段是否發(fā)生改變,在講述跨段操作時會還會詳述摊聋。threashold用來表示需要進(jìn)行rehash的界限值鸡捐。table數(shù)組存儲段中節(jié)點,每個數(shù)組元素是個hash鏈麻裁,用HashEntry表示箍镜。table也是volatile源祈,這使得能夠讀取到最新的 table值而不需要同步。loadFactor表示負(fù)載因子色迂。

刪除操作remove(key)

public V remove(Object key) {  
   hash = hash(key.hashCode());   
   return segmentFor(hash).remove(key, hash, null);   
}

整個操作是先定位到段香缺,然后委托給段的remove操作。當(dāng)多個刪除操作并發(fā)進(jìn)行時歇僧,只要它們所在的段不相同图张,它們就可以同時進(jìn)行。
下面是Segment的remove方法實現(xiàn):

V remove(Object key, int hash, Object value) {  
     lock();  
     try {  
         int c = count - 1;  
         HashEntry<K,V>[] tab = table;  
         int index = hash & (tab.length - 1);  
         HashEntry<K,V> first = tab[index];  
         HashEntry<K,V> e = first;  
         while (e != null && (e.hash != hash || !key.equals(e.key)))  
             e = e.next;  
         V oldValue = null;  
         if (e != null) {  
             V v = e.value;  
             if (value == null || value.equals(v)) {  
                 oldValue = v;  

                 // All entries following removed node can stay  
                 // in list, but all preceding ones need to be  
                 // cloned.  
                 ++modCount;  
                 HashEntry<K,V> newFirst = e.next;  
                 *for (HashEntry<K,V> p = first; p != e; p = p.next)  
                     *newFirst = new HashEntry<K,V>(p.key, p.hash,  
                                                   newFirst, p.value);  
                 tab[index] = newFirst;  
                 count = c; // write-volatile  
             }  
         }  
         return oldValue;  
     } finally {  
         unlock();  
     }  
 }

整個操作是在持有段鎖的情況下執(zhí)行的诈悍,空白行之前的行主要是定位到要刪除的節(jié)點e祸轮。接下來,如果不存在這個節(jié)點就直接返回null侥钳,否則就要將e前面的結(jié)點復(fù)制一遍适袜,尾結(jié)點指向e的下一個結(jié)點。e后面的結(jié)點不需要復(fù)制舷夺,它們可以重用苦酱。
中間那個for循環(huán)是做什么用的呢?(*號標(biāo)記)從代碼來看给猾,就是將定位之后的所有entry克隆并拼回前面去疫萤,但有必要嗎?每次刪除一個元素就要將那之前的元素克隆一遍耙册?這點其實是由entry的不變性來決定的给僵,仔細(xì)觀察entry定義,發(fā)現(xiàn)除了value详拙,其他所有屬性都是用final來修飾的,這意味著在第一次設(shè)置了next域之后便不能再改變它蔓同,取而代之的是將它之前的節(jié)點全都克隆一次饶辙。至于entry為什么要設(shè)置為不變性,這跟不變性的訪問不需要同步從而節(jié)省時間有關(guān)
下面是個示意圖

刪除元素之前:
image.png
刪除元素3之后:
image.png

get操作

ConcurrentHashMap的get操作是直接委托給Segment的get方法斑粱,直接看Segment的get方法:

V get(Object key, int hash) {  
     if (count != 0) { // read-volatile 當(dāng)前桶的數(shù)據(jù)個數(shù)是否為0 
         HashEntry<K,V> e = getFirst(hash);  得到頭節(jié)點
         while (e != null) {  
             if (e.hash == hash && key.equals(e.key)) {  
                 V v = e.value;  
                 if (v != null)  
                     return v;  
                 return readValueUnderLock(e); // recheck  
             }  
             e = e.next;  
         }  
     }  
     returnnull;  
 }

get操作不需要鎖弃揽。
  除非讀到的值是空的才會加鎖重讀,我們知道HashTable容器的get方法是需要加鎖的则北,那么ConcurrentHashMap的get操作是如何做到不加鎖的呢矿微?原因是它的get方法里將要使用的共享變量都定義成volatile

第一步是訪問count變量,這是一個volatile變量尚揣,由于所有的修改操作在進(jìn)行結(jié)構(gòu)修改時都會在最后一步寫count 變量涌矢,通過這種機(jī)制保證get操作能夠得到幾乎最新的結(jié)構(gòu)更新。對于非結(jié)構(gòu)更新快骗,也就是結(jié)點值的改變娜庇,由于HashEntry的value變量是 volatile的塔次,也能保證讀取到最新的值。

接下來就是根據(jù)hash和key對hash鏈進(jìn)行遍歷找到要獲取的結(jié)點名秀,如果沒有找到励负,直接訪回null。對hash鏈進(jìn)行遍歷不需要加鎖的原因在于鏈指針next是final的匕得。但是頭指針卻不是final的继榆,這是通過getFirst(hash)方法返回,也就是存在 table數(shù)組中的值汁掠。這使得getFirst(hash)可能返回過時的頭結(jié)點裕照,例如,當(dāng)執(zhí)行g(shù)et方法時调塌,剛執(zhí)行完getFirst(hash)之后晋南,另一個線程執(zhí)行了刪除操作并更新頭結(jié)點,這就導(dǎo)致get方法中返回的頭結(jié)點不是最新的羔砾。這是可以允許负间,通過對count變量的協(xié)調(diào)機(jī)制,get能讀取到幾乎最新的數(shù)據(jù)姜凄,雖然可能不是最新的政溃。要得到最新的數(shù)據(jù),只有采用完全的同步态秧。

最后董虱,如果找到了所求的結(jié)點,判斷它的值如果非空就直接返回申鱼,否則在有鎖的狀態(tài)下再讀一次愤诱。這似乎有些費解,理論上結(jié)點的值不可能為空捐友,這是因為 put的時候就進(jìn)行了判斷淫半,如果為空就要拋NullPointerException∠蛔空值的唯一源頭就是HashEntry中的默認(rèn)值科吭,因為 HashEntry中的value不是final的,非同步讀取有可能讀取到空值猴鲫。仔細(xì)看下put操作的語句:tab[index] = new HashEntry<K,V>(key, hash, first, value)对人,在這條語句中,HashEntry構(gòu)造函數(shù)中對value的賦值以及對tab[index]的賦值可能被重新排序拂共,這就可能導(dǎo)致結(jié)點的值為空牺弄。這里當(dāng)v為空時,可能是一個線程正在改變節(jié)點匣缘,而之前的get操作都未進(jìn)行鎖定猖闪,根據(jù)bernstein條件鲜棠,讀后寫或?qū)懞笞x都會引起數(shù)據(jù)的不一致,所以這里要對這個e重新上鎖再讀一遍培慌,以保證得到的是正確值豁陆。

V readValueUnderLock(HashEntry<K,V> e) {  
     lock();  
     try {  
         return e.value;  
     } finally {  
         unlock();  
     }  
 }

如用于統(tǒng)計當(dāng)前Segement大小的count字段和用于存儲值的HashEntry的value。定義成volatile的變量吵护,能夠在線程之間保持可見性盒音,能夠被多線程同時讀,并且保證不會讀到過期的值馅而,但是只能被單線程寫(有一種情況可以被多線程寫祥诽,就是寫入的值不依賴于原值),在get操作里只需要讀不需要寫共享變量count和value瓮恭,所以可以不用加鎖雄坪。之所以不會讀到過期的值,是根據(jù)java內(nèi)存模型的happen before原則屯蹦,對volatile字段的寫入操作先于讀操作维哈,即使兩個線程同時修改和獲取volatile變量,get操作也能拿到最新的值登澜,這是用volatile替換鎖的經(jīng)典應(yīng)用場景

put操作

同樣地put操作也是委托給段的put方法阔挠。下面是段的put方法:

V put(K key, int hash, V value, boolean onlyIfAbsent) {  
     lock();  
     try {  
         int c = count;  
         if (c++ > threshold) // ensure capacity  
             rehash();  
         HashEntry<K,V>[] tab = table;  
         int index = hash & (tab.length - 1);  
         HashEntry<K,V> first = tab[index];  
         HashEntry<K,V> e = first;  
         while (e != null && (e.hash != hash || !key.equals(e.key)))  
             e = e.next;  
         V oldValue;  
         if (e != null) {  
             oldValue = e.value;  
             if (!onlyIfAbsent)  
                 e.value = value;  
         }  
         else {  
             oldValue = null;  
             ++modCount;  
             tab[index] = new HashEntry<K,V>(key, hash, first, value);  
             count = c; // write-volatile  
         }  
         return oldValue;  
     } finally {  
         unlock();  
     }  
 }

該方法也是在持有段鎖(鎖定整個segment)的情況下執(zhí)行的,這當(dāng)然是為了并發(fā)的安全脑蠕,修改數(shù)據(jù)是不能并發(fā)進(jìn)行的购撼,必須得有個判斷是否超限的語句以確保容量不足時能夠rehash。接著是找是否存在同樣一個key的結(jié)點谴仙,如果存在就直接替換這個結(jié)點的值迂求。否則創(chuàng)建一個新的結(jié)點并添加到hash鏈的頭部,這時一定要修改modCount和count的值狞甚,同樣修改count的值一定要放在最后一步锁摔。put方法調(diào)用了rehash方法,reash方法實現(xiàn)得也很精巧哼审,主要利用了table的大小為2^n,這里就不介紹了孕豹。而比較難懂的是這句int index = hash & (tab.length - 1)涩盾,原來segment里面才是真正的hashtable,即每個segment是一個傳統(tǒng)意義上的hashtable,如上圖励背,從兩者的結(jié)構(gòu)就可以看出區(qū)別春霍,這里就是找出需要的entry在table的哪一個位置,之后得到的entry就是這個鏈的第一個節(jié)點叶眉,如果e!=null址儒,說明找到了芹枷,這是就要替換節(jié)點的值(onlyIfAbsent == false),否則莲趣,我們需要new一個entry鸳慈,它的后繼是first,而讓tab[index]指向它喧伞,什么意思呢走芋?實際上就是將這個新entry插入到鏈頭,剩下的就非常容易理解了

由于put方法里需要對共享變量進(jìn)行寫入操作潘鲫,所以為了線程安全翁逞,在操作共享變量時必須得加鎖。Put方法首先定位到Segment溉仑,然后在Segment里進(jìn)行插入操作挖函。插入操作需要經(jīng)歷兩個步驟,第一步判斷是否需要對Segment里的HashEntry數(shù)組進(jìn)行擴(kuò)容浊竟,第二步定位添加元素的位置然后放在HashEntry數(shù)組里怨喘。

Concurrenthashmap和hashmap擴(kuò)容

  • \old),如果超過閥值逐沙,數(shù)組進(jìn)行擴(kuò)容哲思。值得一提的是,Segment的擴(kuò)容判斷比HashMap更恰當(dāng)吩案,因為HashMap是在插入元素后判斷元素是否已經(jīng)到達(dá)容量的棚赔,如果到達(dá)了就進(jìn)行擴(kuò)容,但是很有可能擴(kuò)容之后沒有新元素插入徘郭,這時HashMap就進(jìn)行了一次無效的擴(kuò)容靠益。
  • Concurrenthashmap如何擴(kuò)容央勒。擴(kuò)容的時候首先會創(chuàng)建一個兩倍于原容量的數(shù)組哄啄,然后將原數(shù)組里的元素進(jìn)行再hash后插入到新的數(shù)組里奕污。為了高效ConcurrentHashMap不會對整個容器進(jìn)行擴(kuò)容尺借,而只對某個segment進(jìn)行擴(kuò)容杈抢。

另一個操作是containsKey敬拓,這個實現(xiàn)就要簡單得多了洼专,因為它不需要讀取值:

boolean containsKey(Object key, int hash) {  
     if (count != 0) { // read-volatile  
         HashEntry<K,V> e = getFirst(hash);  
         while (e != null) {  
             if (e.hash == hash && key.equals(e.key))  
                 returntrue;  
             e = e.next;  
         }  
     }  
     returnfalse;  
 }

size()操作

如果我們要統(tǒng)計整個ConcurrentHashMap里元素的大小贞绵,就必須統(tǒng)計所有Segment里元素的大小后求和镇草。Segment里的全局變量count是一個volatile變量眶痰,那么在多線程場景下,我們是不是直接把所有Segment的count相加就可以得到整個ConcurrentHashMap大小了呢梯啤?不是的竖伯,雖然相加時可以獲取每個Segment的count的最新值,但是拿到之后可能累加前使用的count發(fā)生了變化,那么統(tǒng)計結(jié)果就不準(zhǔn)了七婴。所以最安全的做法祟偷,是在統(tǒng)計size的時候把所有Segment的put,remove和clean方法全部鎖住打厘,但是這種做法顯然非常低效修肠。
  因為在累加count操作過程中,之前累加過的count發(fā)生變化的幾率非常小婚惫,所以ConcurrentHashMap的做法是先嘗試2次通過不鎖住Segment的方式來統(tǒng)計各個Segment大小氛赐,如果統(tǒng)計的過程中,容器的count發(fā)生了變化先舷,則再采用加鎖的方式來統(tǒng)計所有Segment的大小艰管。
  那么ConcurrentHashMap是如何判斷在統(tǒng)計的時候容器是否發(fā)生了變化呢?使用modCount變量蒋川,在put , remove和clean方法里操作元素前都會將變量modCount進(jìn)行加1牲芋,那么在統(tǒng)計size前后比較modCount是否發(fā)生變化,從而得知容器的大小是否發(fā)生變化捺球。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末缸浦,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子氮兵,更是在濱河造成了極大的恐慌裂逐,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泣栈,死亡現(xiàn)場離奇詭異卜高,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)南片,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進(jìn)店門掺涛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人疼进,你說我怎么就攤上這事薪缆。” “怎么了伞广?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵拣帽,是天一觀的道長。 經(jīng)常有香客問我嚼锄,道長诞外,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任灾票,我火速辦了婚禮,結(jié)果婚禮上茫虽,老公的妹妹穿的比我還像新娘刊苍。我一直安慰自己既们,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布正什。 她就那樣靜靜地躺著啥纸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪婴氮。 梳的紋絲不亂的頭發(fā)上斯棒,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天,我揣著相機(jī)與錄音主经,去河邊找鬼荣暮。 笑死,一個胖子當(dāng)著我的面吹牛罩驻,可吹牛的內(nèi)容都是我干的穗酥。 我是一名探鬼主播,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼惠遏,長吁一口氣:“原來是場噩夢啊……” “哼砾跃!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起节吮,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤抽高,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后透绩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體翘骂,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年渺贤,在試婚紗的時候發(fā)現(xiàn)自己被綠了雏胃。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡志鞍,死狀恐怖瞭亮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情固棚,我是刑警寧澤统翩,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站此洲,受9級特大地震影響厂汗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜呜师,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一娶桦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦衷畦、人聲如沸栗涂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽斤程。三九已至,卻和暖如春菩混,著一層夾襖步出監(jiān)牢的瞬間忿墅,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工沮峡, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留疚脐,地道東北人。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓帖烘,卻偏偏與公主長得像亮曹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子秘症,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,871評論 2 354