亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專(zhuān)欄INFORMATION COLUMN

Java集合---ConcurrentHashMap原理分析

Yangyang / 3022人閱讀

摘要:再哈希,其目的是為了減少哈希沖突,使元素能夠均勻的分布在不同的上,從而提高容器的存取效率。我們?cè)侔焉厦娴亩M(jìn)制數(shù)據(jù)進(jìn)行再哈希后結(jié)果如下,為了方便閱讀,不足位的高位補(bǔ)了,每隔四位用豎線分割下。通過(guò)以下哈希算法定位。

哪有什么天生如此只是我們天天堅(jiān)持。

一、背景 線程不安全的HashMap

因?yàn)槎嗑€程環(huán)境下,使用Hashmap進(jìn)行put操作會(huì)引起死循環(huán),導(dǎo)致CPU利用率接近100%,所以在并發(fā)情況下不能使用HashMap。

效率低下的HashTable容器

HashTable容器使用synchronized來(lái)保證線程安全,但在線程競(jìng)爭(zhēng)激烈的情況下HashTable的效率非常低下。因?yàn)楫?dāng)一個(gè)線程訪問(wèn)HashTable的同步方法時(shí),其他線程訪問(wèn)HashTable的同步方法時(shí),可能會(huì)進(jìn)入阻塞或輪詢狀態(tài)。如線程1使用put進(jìn)行添加元素,線程2不但不能使用put方法添加元素,并且也不能使用get方法來(lái)獲取元素,所以競(jìng)爭(zhēng)越激烈效率越低。

鎖分段技術(shù)

HashTable容器在競(jìng)爭(zhēng)激烈的并發(fā)環(huán)境下表現(xiàn)出效率低下的原因,是因?yàn)樗性L問(wèn)HashTable的線程都必須競(jìng)爭(zhēng)同一把鎖,那假如容器里有多把鎖,每一把鎖用于鎖容器其中一部分?jǐn)?shù)據(jù),那么當(dāng)多線程訪問(wèn)容器里不同數(shù)據(jù)段的數(shù)據(jù)時(shí),線程間就不會(huì)存在鎖競(jìng)爭(zhēng),從而可以有效的提高并發(fā)訪問(wèn)效率,
這就是ConcurrentHashMap所使用的鎖分段技術(shù),首先將數(shù)據(jù)分成一段一段的存儲(chǔ),然后給每一段數(shù)據(jù)配一把鎖,當(dāng)一個(gè)線程占用鎖訪問(wèn)其中一個(gè)段數(shù)據(jù)的時(shí)候,其他段的數(shù)據(jù)也能被其他線程訪問(wèn)。有些方法需要跨段,比如size()和containsValue(),它們可能需要鎖定整個(gè)表而而不僅僅是某個(gè)段,這需要按順序鎖定所有段,操作完畢后,又按順序釋放所有段的鎖。這里“按順序”是很重要的,否則極有可能出現(xiàn)死鎖,在ConcurrentHashMap內(nèi)部,段數(shù)組是final的,并且其成員變量實(shí)際上也是final的,但是,僅僅是將數(shù)組聲明為final的并不保證數(shù)組成員也是final的,這需要實(shí)現(xiàn)上的保證。這可以確保不會(huì)出現(xiàn)死鎖,因?yàn)?strong>獲得鎖的順序是固定的。

oncurrentHashMap是由Segment數(shù)組結(jié)構(gòu)和HashEntry數(shù)組結(jié)構(gòu)組成。Segment是一種可重入鎖ReentrantLock,在ConcurrentHashMap里扮演鎖的角色,HashEntry則用于存儲(chǔ)鍵值對(duì)數(shù)據(jù)。一個(gè)ConcurrentHashMap里包含一個(gè)Segment數(shù)組,Segment的結(jié)構(gòu)和HashMap類(lèi)似,是一種數(shù)組和鏈表結(jié)構(gòu), 一個(gè)Segment里包含一個(gè)HashEntry數(shù)組,每個(gè)HashEntry是一個(gè)鏈表結(jié)構(gòu)的元素, 每個(gè)Segment守護(hù)者一個(gè)HashEntry數(shù)組里的元素,當(dāng)對(duì)HashEntry數(shù)組的數(shù)據(jù)進(jìn)行修改時(shí),必須首先獲得它對(duì)應(yīng)的Segment鎖。

二、應(yīng)用場(chǎng)景

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

三、源碼解讀

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

/** 
* The segments, each of which is a specialized hash table 
*/  
final Segment[] segments;
不變(Immutable)和易變(Volatile)

ConcurrentHashMap完全允許多個(gè)讀操作并發(fā)進(jìn)行,讀操作并不需要加鎖。如果使用傳統(tǒng)的技術(shù),如HashMap中的實(shí)現(xiàn),如果允許可以在hash鏈的中間添加或刪除元素,讀操作不加鎖將得到不一致的數(shù)據(jù)。ConcurrentHashMap實(shí)現(xiàn)技術(shù)是保證HashEntry幾乎是不可變的。HashEntry代表每個(gè)hash鏈中的一個(gè)節(jié)點(diǎn),其結(jié)構(gòu)如下所示:

 static final class HashEntry {  
     final K key;  
     final int hash;  
     volatile V value;  
     final HashEntry next;  
 }

可以看到除了value不是final的,其它值都是final的,這意味著不能從hash鏈的中間或尾部添加或刪除節(jié)點(diǎn),因?yàn)檫@需要修改next 引用值,所有的節(jié)點(diǎn)的修改只能從頭部開(kāi)始。
對(duì)于put操作,可以一律添加到Hash鏈的頭部。
但是對(duì)于remove操作,可能需要從中間刪除一個(gè)節(jié)點(diǎn),這就需要將要?jiǎng)h除節(jié)點(diǎn)的前面所有節(jié)點(diǎn)整個(gè)復(fù)制一遍,最后一個(gè)節(jié)點(diǎn)指向要?jiǎng)h除結(jié)點(diǎn)的下一個(gè)結(jié)點(diǎn)。這在講解刪除操作時(shí)還會(huì)詳述。為了確保讀操作能夠看到最新的值,將value設(shè)置成volatile,這避免了加鎖。

其它

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

定位操作:
final Segment segmentFor(int hash) {  
     return segments[(hash >>> segmentShift) & segmentMask];  
 }

既然ConcurrentHashMap使用分段鎖Segment來(lái)保護(hù)不同段的數(shù)據(jù),那么在插入和獲取元素的時(shí)候,必須先通過(guò)哈希算法定位到Segment??梢钥吹紺oncurrentHashMap會(huì)首先使用Wang/Jenkins hash的變種算法對(duì)元素的hashCode進(jìn)行一次再哈希。
再哈希,其目的是為了減少哈希沖突,使元素能夠均勻的分布在不同的Segment上,從而提高容器的存取效率。假如哈希的質(zhì)量差到極點(diǎn),那么所有的元素都在一個(gè)Segment中,不僅存取元素緩慢,分段鎖也會(huì)失去意義。我做了一個(gè)測(cè)試,不通過(guò)再哈希而直接執(zhí)行哈希計(jì)算:

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);

計(jì)算后輸出的哈希值全是15,通過(guò)這個(gè)例子可以發(fā)現(xiàn)如果不進(jìn)行再哈希,哈希沖突會(huì)非常嚴(yán)重,因?yàn)橹灰臀灰粯樱瑹o(wú)論高位是什么數(shù),其哈希值總是一樣。我們?cè)侔焉厦娴亩M(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ù)都散列開(kāi)了,通過(guò)這種再哈希能讓數(shù)字的每一位都能參加到哈希運(yùn)算當(dāng)中,從而減少哈希沖突。ConcurrentHashMap通過(guò)以下哈希算法定位segment。
默認(rèn)情況下segmentShift為28,segmentMask為15,再哈希后的數(shù)最大是32位二進(jìn)制數(shù)據(jù),向右無(wú)符號(hào)移動(dòng)28位,意思是讓高4位參與到hash運(yùn)算中, (hash >>> segmentShift) & segmentMask的運(yùn)算結(jié)果分別是4,15,7和8,可以看到hash值沒(méi)有發(fā)生沖突。

final Segment segmentFor(int hash) {
    return segments[(hash >>> segmentShift) & segmentMask];
}
數(shù)據(jù)結(jié)構(gòu)

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

static final class Segment 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 (int)(capacity * 
          * loadFactor).) 
          */
         transient int threshold;  
         /** 
          * The per-segment table. 
          */
         transient volatile HashEntry[] 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用來(lái)統(tǒng)計(jì)該段數(shù)據(jù)的個(gè)數(shù),它是volatile,它用來(lái)協(xié)調(diào)修改和讀取操作,以保證讀取操作能夠讀取到幾乎最新的修改。
協(xié)調(diào)方式是這樣的,每次修改操作做了結(jié)構(gòu)上的改變,如增加/刪除節(jié)點(diǎn)(修改節(jié)點(diǎn)的值不算結(jié)構(gòu)上的改變),都要寫(xiě)count值,每次讀取操作開(kāi)始都要讀取count的值。
這利用了 Java 5中對(duì)volatile語(yǔ)義的增強(qiáng),對(duì)同一個(gè)volatile變量的寫(xiě)和讀存在happens-before關(guān)系。
modCount統(tǒng)計(jì)段結(jié)構(gòu)改變的次數(shù),主要是為了檢測(cè)對(duì)多個(gè)段進(jìn)行遍歷過(guò)程中某個(gè)段是否發(fā)生改變,在講述跨段操作時(shí)會(huì)還會(huì)詳述。
threashold用來(lái)表示需要進(jìn)行rehash的界限值。
table數(shù)組存儲(chǔ)段中節(jié)點(diǎn),每個(gè)數(shù)組元素是個(gè)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);   
}

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

V remove(Object key, int hash, Object value) {  
     lock();  
     try {  
         int c = count - 1;  
         HashEntry[] tab = table;  
         int index = hash & (tab.length - 1);  
         HashEntry first = tab[index];  
         HashEntry 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 newFirst = e.next;  
                 *for (HashEntry p = first; p != e; p = p.next)  
                     *newFirst = new HashEntry(p.key, p.hash,  
                                                   newFirst, p.value);  
                 tab[index] = newFirst;  
                 count = c; // write-volatile  
             }  
         }  
         return oldValue;  
     } finally {  
         unlock();  
     }  
 }

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

刪除元素之前:

刪除元素3之后:

第二個(gè)圖其實(shí)有點(diǎn)問(wèn)題,復(fù)制的結(jié)點(diǎn)中應(yīng)該是值為2的結(jié)點(diǎn)在前面,值為1的結(jié)點(diǎn)在后面,也就是剛好和原來(lái)結(jié)點(diǎn)順序相反,還好這不影響我們的討論。
整個(gè)remove實(shí)現(xiàn)并不復(fù)雜,但是需要注意如下幾點(diǎn)。

第一當(dāng)要?jiǎng)h除的結(jié)點(diǎn)存在時(shí),刪除的最后一步操作要將count的值減一。這必須是最后一步操作,否則讀取操作可能看不到之前對(duì)段所做的結(jié)構(gòu)性修改。

第二,remove執(zhí)行的開(kāi)始就將table賦給一個(gè)局部變量tab,這是因?yàn)閠able是 volatile變量,讀寫(xiě)volatile變量的開(kāi)銷(xiāo)很大。編譯器也不能對(duì)volatile變量的讀寫(xiě)做任何優(yōu)化,直接多次訪問(wèn)非volatile實(shí)例變量沒(méi)有多大影響,編譯器會(huì)做相應(yīng)優(yōu)化。

,
`

get操作

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

V get(Object key, int hash) {  
     if (count != 0) { // read-volatile 當(dāng)前桶的數(shù)據(jù)個(gè)數(shù)是否為0 
         HashEntry e = getFirst(hash);  得到頭節(jié)點(diǎn)
         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操作不需要鎖。
  除非讀到的值是空的才會(huì)加鎖重讀,我們知道HashTable容器的get方法是需要加鎖的,那么ConcurrentHashMap的get操作是如何做到不加鎖的呢?原因是它的get方法里將要使用的共享變量都定義成volatile

  第一步是訪問(wèn)count變量,這是一個(gè)volatile變量,由于所有的修改操作在進(jìn)行結(jié)構(gòu)修改時(shí)都會(huì)在最后一步寫(xiě)count 變量,通過(guò)這種機(jī)制保證get操作能夠得到幾乎最新的結(jié)構(gòu)更新。對(duì)于非結(jié)構(gòu)更新,也就是結(jié)點(diǎn)值的改變,由于HashEntry的value變量是 volatile的,也能保證讀取到最新的值。

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

  最后,如果找到了所求的結(jié)點(diǎn),判斷它的值如果非空就直接返回,否則在有鎖的狀態(tài)下再讀一次。這似乎有些費(fèi)解,理論上結(jié)點(diǎn)的值不可能為空,這是因?yàn)?put的時(shí)候就進(jìn)行了判斷,如果為空就要拋NullPointerException??罩档奈ㄒ辉搭^就是HashEntry中的默認(rèn)值,因?yàn)?HashEntry中的value不是final的,非同步讀取有可能讀取到空值。仔細(xì)看下put操作的語(yǔ)句:tab[index] = new HashEntry(key, hash, first, value),在這條語(yǔ)句中,HashEntry構(gòu)造函數(shù)中對(duì)value的賦值以及對(duì)tab[index]的賦值可能被重新排序,這就可能導(dǎo)致結(jié)點(diǎn)的值為空。這里當(dāng)v為空時(shí),可能是一個(gè)線程正在改變節(jié)點(diǎn),而之前的get操作都未進(jìn)行鎖定,根據(jù)bernstein條件,讀后寫(xiě)或?qū)懞笞x都會(huì)引起數(shù)據(jù)的不一致,所以這里要對(duì)這個(gè)e重新上鎖再讀一遍,以保證得到的是正確值。

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

如用于統(tǒng)計(jì)當(dāng)前Segement大小的count字段和用于存儲(chǔ)值的HashEntry的value。定義成volatile的變量,能夠在線程之間保持可見(jiàn)性,能夠被多線程同時(shí)讀,并且保證不會(huì)讀到過(guò)期的值,但是只能被單線程寫(xiě)(有一種情況可以被多線程寫(xiě),就是寫(xiě)入的值不依賴于原值),在get操作里只需要讀不需要寫(xiě)共享變量count和value,所以可以不用加鎖。之所以不會(huì)讀到過(guò)期的值,是根據(jù)java內(nèi)存模型的happen before原則,對(duì)volatile字段的寫(xiě)入操作先于讀操作,即使兩個(gè)線程同時(shí)修改和獲取volatile變量,get操作也能拿到最新的值,這是用volatile替換鎖的經(jīng)典應(yīng)用場(chǎ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[] tab = table;  
         int index = hash & (tab.length - 1);  
         HashEntry first = tab[index];  
         HashEntry 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(key, hash, first, value);  
             count = c; // write-volatile  
         }  
         return oldValue;  
     } finally {  
         unlock();  
     }  
 }

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

  由于put方法里需要對(duì)共享變量進(jìn)行寫(xiě)入操作,所以為了線程安全,在操作共享變量時(shí)必須得加鎖
Put方法首先定位到Segment,然后在Segment里進(jìn)行插入操作。插入操作需要經(jīng)歷兩個(gè)步驟,第一步判斷是否需要對(duì)Segment里的HashEntry數(shù)組進(jìn)行擴(kuò)容,第二步定位添加元素的位置然后放在HashEntry數(shù)組里。
是否需要擴(kuò)容。在插入元素前會(huì)先判斷Segment里的HashEntry數(shù)組是否超過(guò)容量(threshold),如果超過(guò)閥值,數(shù)組進(jìn)行擴(kuò)容。值得一提的是,Segment的擴(kuò)容判斷比HashMap更恰當(dāng),因?yàn)镠ashMap是在插入元素后判斷元素是否已經(jīng)到達(dá)容量的,如果到達(dá)了就進(jìn)行擴(kuò)容,但是很有可能擴(kuò)容之后沒(méi)有新元素插入,這時(shí)HashMap就進(jìn)行了一次無(wú)效的擴(kuò)容。
`如何擴(kuò)容?擴(kuò)容的時(shí)候首先會(huì)創(chuàng)建一個(gè)兩倍于原容量的數(shù)組,然后將原數(shù)組里的元素進(jìn)行再hash后插入到新的數(shù)組里。為了高效ConcurrentHashMap不會(huì)對(duì)整個(gè)容器進(jìn)行擴(kuò)容,而只對(duì)某個(gè)segment進(jìn)行擴(kuò)容。

另一個(gè)操作是containsKey,這個(gè)實(shí)現(xiàn)就要簡(jiǎn)單得多了,因?yàn)樗恍枰x取值:

boolean containsKey(Object key, int hash) {  
     if (count != 0) { // read-volatile  
         HashEntry e = getFirst(hash);  
         while (e != null) {  
             if (e.hash == hash && key.equals(e.key))  
                 returntrue;  
             e = e.next;  
         }  
     }  
     return false;  
 }
size()操作

如果我們要統(tǒng)計(jì)整個(gè)ConcurrentHashMap里元素的大小,就必須統(tǒng)計(jì)所有Segment里元素的大小后求和。

Segment里的全局變量count是一個(gè)volatile變量,那么在多線程場(chǎng)景下,我們是不是直接把所有Segment的count相加就可以得到整個(gè)ConcurrentHashMap大小了呢?
不是的,雖然相加時(shí)可以獲取每個(gè)Segment的count的最新值,但是拿到之后可能累加前使用的count發(fā)生了變化,那么統(tǒng)計(jì)結(jié)果就不準(zhǔn)了。所以最安全的做法,是在統(tǒng)計(jì)size的時(shí)候把所有Segment的put,remove和clean方法全部鎖住,但是這種做法顯然非常低效。

  因?yàn)樵诶奂觕ount操作過(guò)程中,之前累加過(guò)的count發(fā)生變化的幾率非常小,所以ConcurrentHashMap的做法是先嘗試2次通過(guò)不鎖住Segment的方式來(lái)統(tǒng)計(jì)各個(gè)Segment大小,如果統(tǒng)計(jì)的過(guò)程中,容器的count發(fā)生了變化,則再采用加鎖的方式來(lái)統(tǒng)計(jì)所有Segment的大小。

  那么ConcurrentHashMap是如何判斷在統(tǒng)計(jì)的時(shí)候容器是否發(fā)生了變化呢?
使用modCount變量,在put , remove和clean方法里操作元素前都會(huì)將變量modCount進(jìn)行加1,那么在統(tǒng)計(jì)size前后比較modCount是否發(fā)生變化,從而得知容器的大小是否發(fā)生變化。

其他資料: ConcurrentHashMap

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/68027.html

相關(guān)文章

  • 這幾道Java集合框架面試題在面試中幾乎必問(wèn)

    摘要:若遇到哈希沖突,則將沖突的值加到鏈表中即可。之后相比于之前的版本,之后在解決哈希沖突時(shí)有了較大的變化,當(dāng)鏈表長(zhǎng)度大于閾值默認(rèn)為時(shí),將鏈表轉(zhuǎn)化為紅黑樹(shù),以減少搜索時(shí)間。有序,唯一紅黑樹(shù)自平衡的排序二叉樹(shù)。 本文是最最最常見(jiàn)Java面試題總結(jié)系列第三周的文章。主要內(nèi)容: Arraylist 與 LinkedList 異同 ArrayList 與 Vector 區(qū)別 HashMap的底層...

    bigdevil_s 評(píng)論0 收藏0
  • 周末去面試,進(jìn)去 5 分鐘就出來(lái)了…

    摘要:記得,那是一個(gè)周末,棧長(zhǎng)去某知名互聯(lián)網(wǎng)公司面試,好像不到五分鐘,我就被面試官親切地趕出來(lái)了,當(dāng)時(shí)我那個(gè)尷尬,內(nèi)心深受打擊。。。 你們可能會(huì)想,棧長(zhǎng)這么菜的嗎?5分鐘都堅(jiān)持不了? 本文說(shuō)起來(lái)會(huì)有點(diǎn)尷尬,畢竟這是棧長(zhǎng)我曾經(jīng)經(jīng)歷過(guò)的故事。。。 那時(shí)候的棧長(zhǎng)還真菜,每天寫(xiě)著 if/ for 及一些簡(jiǎn)單的業(yè)務(wù)邏輯代碼,雖工作有些日子了,但技術(shù)水平還停留在剛畢業(yè)的起步階段。。。 記得,那是一個(gè)周末...

    pf_miles 評(píng)論0 收藏0
  • Netflix Hystrix斷路器原理分析

    摘要:斷路器原理斷路器在和執(zhí)行過(guò)程中起到至關(guān)重要的作用。其中通過(guò)來(lái)定義,每一個(gè)命令都需要有一個(gè)來(lái)標(biāo)識(shí),同時(shí)根據(jù)這個(gè)可以找到對(duì)應(yīng)的斷路器實(shí)例。一個(gè)啥都不做的斷路器,它允許所有請(qǐng)求通過(guò),并且斷路器始終處于閉合狀態(tài)斷路器的另一個(gè)實(shí)現(xiàn)類(lèi)。 斷路器原理 斷路器在HystrixCommand和HystrixObservableCommand執(zhí)行過(guò)程中起到至關(guān)重要的作用。查看一下核心組件HystrixCi...

    Lemon_95 評(píng)論0 收藏0
  • 一名3年工作經(jīng)驗(yàn)的java程序員應(yīng)該具備的職業(yè)技能

    摘要:一名年工作經(jīng)驗(yàn)的程序員應(yīng)該具備的技能,這可能是程序員們比較關(guān)心的內(nèi)容。數(shù)據(jù)結(jié)構(gòu)和算法分析數(shù)據(jù)結(jié)構(gòu)和算法分析,對(duì)于一名程序員來(lái)說(shuō),會(huì)比不會(huì)好而且在工作中能派上用場(chǎng)。 一名3年工作經(jīng)驗(yàn)的Java程序員應(yīng)該具備的技能,這可能是Java程序員們比較關(guān)心的內(nèi)容。我這里要說(shuō)明一下,以下列舉的內(nèi)容不是都要會(huì)的東西—-但是如果你掌握得越多,最終能得到的評(píng)價(jià)、拿到的薪水勢(shì)必也越高。 1、基本語(yǔ)法 這包括...

    renweihub 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<