ConcurrentHashMap,一个更快的HashMap
ConcurrentHashMap?是 Doug Lea 的?util.concurrent?包的一部分,它提供比 Hashtable 或者 synchronizedMap 更高程度的并發(fā)性。而且,對(duì)于大多數(shù)成功的?get()?操作它會(huì)設(shè)法避免完全鎖定,其結(jié)果就是使得并發(fā)應(yīng)用程序有著非常好的吞吐量。這個(gè)月,Brian Goetz 仔細(xì)分析了?ConcurrentHashMap?的代碼,并探討 Doug Lea 是如何在不損失線程安全的情況下取得這么驕人成績(jī)的。請(qǐng)?jiān)?討論論壇?上與作者及其他讀者共享您對(duì)本文的一些想法(也可以在文章的頂部或底部點(diǎn)擊討論來(lái)訪問(wèn)論壇)。
在7月份的那期?Java理論與實(shí)踐(“Concurrent collections classes”)中,我們簡(jiǎn)單地回顧了可伸縮性的瓶頸,并討論了怎么用共享數(shù)據(jù)結(jié)構(gòu)的方法獲得更高的并發(fā)性和吞吐量。有時(shí)候?qū)W習(xí)的最好方法是分析專(zhuān)家的成果,所以這個(gè)月我們將分析 Doug Lea 的util.concurrent?包中的?ConcurrentHashMap?的實(shí)現(xiàn)。JSR 133 將指定?ConcurrentHashMap?的一個(gè)版本,該版本針對(duì) Java 內(nèi)存模型(JMM)作了優(yōu)化,它將包含在 JDK 1.5 的?java.util.concurrent?包中。util.concurrent?中的版本在老的和新的內(nèi)存模型中都已通過(guò)線程安全審核。
針對(duì)吞吐量進(jìn)行優(yōu)化
ConcurrentHashMap?使用了幾個(gè)技巧來(lái)獲得高程度的并發(fā)以及避免鎖定,包括為不同的 hash bucket(桶)使用多個(gè)寫(xiě)鎖和使用 JMM 的不確定性來(lái)最小化鎖被保持的時(shí)間——或者根本避免獲取鎖。對(duì)于大多數(shù)一般用法來(lái)說(shuō)它是經(jīng)過(guò)優(yōu)化的,這些用法往往會(huì)檢索一個(gè)很可能在 map 中已經(jīng)存在的值。事實(shí)上,多數(shù)成功的 get() 操作根本不需要任何鎖定就能運(yùn)行。(警告:不要自己試圖這樣做!想比 JMM 聰明不像看上去的那么容易。util.concurrent?類(lèi)是由并發(fā)專(zhuān)家編寫(xiě)的,并且在 JMM 安全性方面經(jīng)過(guò)了嚴(yán)格的同行評(píng)審。 )
多個(gè)寫(xiě)鎖
我們可以回想一下,Hashtable(或者替代方案?Collections.synchronizedMap)的可伸縮性的主要障礙是它使用了一個(gè) map 范圍(map-wide)的鎖,為了保證插入、刪除或者檢索操作的完整性必須保持這樣一個(gè)鎖,而且有時(shí)候甚至還要為了保證迭代遍歷操作的完整性保持這樣一個(gè)鎖。這樣一來(lái),只要鎖被保持,就從根本上阻止了其他線程訪問(wèn) Map,即使處理器有空閑也不能訪問(wèn),這樣大大地限制了并發(fā)性。
ConcurrentHashMap?摒棄了單一的 map 范圍的鎖,取而代之的是由 32 個(gè)鎖組成的集合,其中每個(gè)鎖負(fù)責(zé)保護(hù) hash bucket 的一個(gè)子集。鎖主要由變化性操作(put()?和?remove())使用。具有 32 個(gè)獨(dú)立的鎖意味著最多可以有 32 個(gè)線程可以同時(shí)修改 map。這并不一定是說(shuō)在并發(fā)地對(duì) map 進(jìn)行寫(xiě)操作的線程數(shù)少于 32 時(shí),另外的寫(xiě)操作不會(huì)被阻塞——32 對(duì)于寫(xiě)線程來(lái)說(shuō)是理論上的并發(fā)限制數(shù)目,但是實(shí)際上可能達(dá)不到這個(gè)值。但是,32 依然比 1 要好得多,而且對(duì)于運(yùn)行于目前這一代的計(jì)算機(jī)系統(tǒng)上的大多數(shù)應(yīng)用程序來(lái)說(shuō)已經(jīng)足夠了。?
map 范圍的操作
有 32 個(gè)獨(dú)立的鎖,其中每個(gè)鎖保護(hù) hash bucket 的一個(gè)子集,這樣需要獨(dú)占訪問(wèn) map 的操作就必須獲得所有32個(gè)鎖。一些 map 范圍的操作,比如說(shuō)?size()?和?isEmpty(),也許能夠不用一次鎖整個(gè) map(通過(guò)適當(dāng)?shù)叵薅ㄟ@些操作的語(yǔ)義),但是有些操作,比如 map 重排(擴(kuò)大 hash bucket 的數(shù)量,隨著 map 的增長(zhǎng)重新分布元素),則必須保證獨(dú)占訪問(wèn)。Java 語(yǔ)言不提供用于獲取可變大小的鎖集合的簡(jiǎn)便方法。必須這么做的情況很少見(jiàn),一旦碰到這種情況,可以用遞歸方法來(lái)實(shí)現(xiàn)。
JMM概述
在進(jìn)入?put()、get()?和?remove()?的實(shí)現(xiàn)之前,讓我們先簡(jiǎn)單地看一下 JMM。JMM 掌管著一個(gè)線程對(duì)內(nèi)存的動(dòng)作 (讀和寫(xiě))影響其他線程對(duì)內(nèi)存的動(dòng)作的方式。由于使用處理器寄存器和預(yù)處理 cache 來(lái)提高內(nèi)存訪問(wèn)速度帶來(lái)的性能提升,Java 語(yǔ)言規(guī)范(JLS)允許一些內(nèi)存操作并不對(duì)于所有其他線程立即可見(jiàn)。有兩種語(yǔ)言機(jī)制可用于保證跨線程內(nèi)存操作的一致性——synchronized?和?volatile。
按照 JLS 的說(shuō)法,“在沒(méi)有顯式同步的情況下,一個(gè)實(shí)現(xiàn)可以自由地更新主存,更新時(shí)所采取的順序可能是出人意料的。”其意思是說(shuō),如果沒(méi)有同步的話,在一個(gè)給定線程中某種順序的寫(xiě)操作對(duì)于另外一個(gè)不同的線程來(lái)說(shuō)可能呈現(xiàn)出不同的順序, 并且對(duì)內(nèi)存變量的更新從一個(gè)線程傳播到另外一個(gè)線程的時(shí)間是不可預(yù)測(cè)的。
雖然使用同步最常見(jiàn)的原因是保證對(duì)代碼關(guān)鍵部分的原子訪問(wèn),但實(shí)際上同步提供三個(gè)獨(dú)立的功能——原子性、可見(jiàn)性和順序性。原子性非常簡(jiǎn)單——同步實(shí)施一個(gè)可重入的(reentrant)互斥,防止多于一個(gè)的線程同時(shí)執(zhí)行由一個(gè)給定的監(jiān)視器保護(hù)的代碼塊。不幸的是,多數(shù)文章都只關(guān)注原子性方面,而忽略了其他方面。但是同步在 JMM 中也扮演著很重要的角色,會(huì)引起 JVM 在獲得和釋放監(jiān)視器的時(shí)候執(zhí)行內(nèi)存壁壘(memory barrier)。
一個(gè)線程在獲得一個(gè)監(jiān)視器之后,它執(zhí)行一個(gè)讀屏障(read barrier)——使得緩存在線程局部?jī)?nèi)存(比如說(shuō)處理器緩存或者處理器寄存器)中的所有變量都失效,這樣就會(huì)導(dǎo)致處理器重新從主存中讀取同步代碼塊使用的變量。與此類(lèi)似,在釋放監(jiān)視器時(shí),線程會(huì)執(zhí)行一個(gè)寫(xiě)屏障(write barrier)——將所有修改過(guò)的變量寫(xiě)回主存。互斥獨(dú)占和內(nèi)存壁壘結(jié)合使用意味著只要您在程序設(shè)計(jì)的時(shí)候遵循正確的同步法則(也就是說(shuō),每當(dāng)寫(xiě)一個(gè)后面可能被其他線程訪問(wèn)的變量,或者讀取一個(gè)可能最后被另一個(gè)線程修改的變量時(shí),都要使用同步),每個(gè)線程都會(huì)得到它所使用的共享變量的正確的值。
如果在訪問(wèn)共享變量的時(shí)候沒(méi)有同步的話,就會(huì)發(fā)生一些奇怪的事情。一些變化可能會(huì)通過(guò)線程立即反映出來(lái),而其他的則需要一些時(shí)間(這由關(guān)聯(lián)緩存的本質(zhì)所致)。結(jié)果,如果沒(méi)有同步您就不能保證內(nèi)存內(nèi)容必定一致(相關(guān)的變量相互間可能會(huì)不一致),或者不能得到當(dāng)前的內(nèi)存內(nèi)容(一些值可能是過(guò)時(shí)的)。避免這種危險(xiǎn)情況的常用方法(也是推薦使用的方法)當(dāng)然是正確地使用同步。然而在有些情況下,比如說(shuō)在像?ConcurrentHashMap?之類(lèi)的一些使用非常廣泛的庫(kù)類(lèi)中,在開(kāi)發(fā)過(guò)程當(dāng)中還需要一些額外的專(zhuān)業(yè)技能和努力(可能比一般的開(kāi)發(fā)要多出很多倍)來(lái)獲得較高的性能。
ConcurrentHashMap 實(shí)現(xiàn)
如前所述,ConcurrentHashMap?使用的數(shù)據(jù)結(jié)構(gòu)與?Hashtable?或?HashMap?的實(shí)現(xiàn)類(lèi)似,是 hash bucket 的一個(gè)可變數(shù)組,每個(gè)?ConcurrentHashMap?都由一個(gè)?Map.Entry?元素鏈構(gòu)成,如清單1所示。與?Hashtable?和?HashMap?不同的是,ConcurrentHashMap?沒(méi)有使用單一的集合鎖(collection lock),而是使用了一個(gè)固定的鎖池,這個(gè)鎖池形成了bucket 集合的一個(gè)分區(qū)。
清單1. ConcurrentHashMap 使用的 Map.Entry 元素
| protected static class Entry implements Map.Entry { protected final Object key; protected volatile Object value; protected final int hash; protected final Entry next; ...} |
不用鎖定遍歷數(shù)據(jù)結(jié)構(gòu)
與?Hashtable?或者典型的鎖池?Map?實(shí)現(xiàn)不同,ConcurrentHashMap.get()?操作不一定需要獲取與相關(guān)bucket 相關(guān)聯(lián)的鎖。如果不使用鎖定,那么實(shí)現(xiàn)必須有能力處理它用到的所有變量的過(guò)時(shí)的或者不一致的值,比如說(shuō)列表頭指針和?Map.Entry?元素的域(包括組成每個(gè) hash bucket 條目的鏈表的鏈接指針)。
大多并發(fā)類(lèi)使用同步來(lái)保證獨(dú)占式訪問(wèn)一個(gè)數(shù)據(jù)結(jié)構(gòu)(以及保持?jǐn)?shù)據(jù)結(jié)構(gòu)的一致性)。ConcurrentHashMap?沒(méi)有采用獨(dú)占性和一致性,它使用的鏈表是經(jīng)過(guò)精心設(shè)計(jì)的,所以其實(shí)現(xiàn)可以檢測(cè)?到它的列表是否一致或者已經(jīng)過(guò)時(shí)。如果它檢測(cè)到它的列表出現(xiàn)不一致或者過(guò)時(shí),或者干脆就找不到它要找的條目,它就會(huì)對(duì)適當(dāng)?shù)腷ucket 鎖進(jìn)行同步并再次搜索整個(gè)鏈。這樣做在一般的情況下可以優(yōu)化查找,所謂的一般情況是指大多數(shù)檢索操作是成功的并且檢索的次數(shù)多于插入和刪除的次數(shù)。
使用不變性
不一致性的一個(gè)重要來(lái)源是可以避免得,其方法是使?Entry?元素接近不變性——除了值字段(它們是易變的)之外,所有字段都是 final 的。這就意味著不能將元素添加到 hash 鏈的中間或末尾,或者從 hash 鏈的中間或末尾刪除元素——而只能從 hash 鏈的開(kāi)頭添加元素,并且刪除操作包括克隆整個(gè)鏈或鏈的一部分并更新列表的頭指針。所以說(shuō)只要有對(duì)某個(gè) hash 鏈的一個(gè)引用,即使可能不知道有沒(méi)有對(duì)列表頭節(jié)點(diǎn)的引用,您也可以知道列表的其余部分的結(jié)構(gòu)不會(huì)改變。而且,因?yàn)橹底侄问且鬃兊?#xff0c;所以能夠立即看到對(duì)值字段的更新,從而大大簡(jiǎn)化了編寫(xiě)能夠處理內(nèi)存潛在過(guò)時(shí)的?Map?的實(shí)現(xiàn)。
新的 JMM 為 final 型變量提供初始化安全,而老的 JMM 不提供,這意味著另一個(gè)線程看到的可能是 final 字段的默認(rèn)值,而不是對(duì)象的構(gòu)造方法提供的值。實(shí)現(xiàn)必須能夠同時(shí)檢測(cè)到這一點(diǎn),這是通過(guò)保證?Entry?中每個(gè)字段的默認(rèn)值不是有效值來(lái)實(shí)現(xiàn)的。這樣構(gòu)造好列表之后,如果任何一個(gè)?Entry?字段有其默認(rèn)值(零或空),搜索就會(huì)失敗,提示同步?get()?并再次遍歷鏈。
檢索操作
檢索操作首先為目標(biāo) bucket 查找頭指針(是在不鎖定的情況下完成的,所以說(shuō)可能是過(guò)時(shí)的),然后在不獲取 bucket 鎖的情況下遍歷 bucket 鏈。如果它不能發(fā)現(xiàn)要查找的值,就會(huì)同步并試圖再次查找條目,如清單2所示:
清單2. ConcurrentHashMap.get() 實(shí)現(xiàn)
| public Object get(Object key) { int hash = hash(key); // throws null pointer exception if key is null // Try first without locking... Entry[] tab = table; int index = hash & (tab.length - 1); Entry first = tab[index]; Entry e; for (e = first; e != null; e = e.next) { if (e.hash == hash && eq(key, e.key)) { Object value = e.value; // null values means that the element has been removed if (value != null) return value; else break; } } // Recheck under synch if key apparently not there or interference Segment seg = segments[hash & SEGMENT_MASK]; synchronized(seg) { tab = table; index = hash & (tab.length - 1); Entry newFirst = tab[index]; if (e != null || first != newFirst) { for (e = newFirst; e != null; e = e.next) { if (e.hash == hash && eq(key, e.key)) return e.value; } } return null; } } |
刪除操作
因?yàn)橐粋€(gè)線程可能看到 hash 鏈中鏈接指針的過(guò)時(shí)的值,簡(jiǎn)單地從鏈中刪除一個(gè)元素不足以保證其他線程在進(jìn)行查找的時(shí)候不繼續(xù)看到被刪除的值。相反,從清單3我們可以看到,刪除操作分兩個(gè)過(guò)程——首先找到適當(dāng)?shù)?Entry?對(duì)象并把其值字段設(shè)為?null,然后對(duì)鏈中從頭元素到要?jiǎng)h除的元素的部分進(jìn)行克隆,再連接到要?jiǎng)h除的元素之后的部分。因?yàn)橹底侄问且鬃兊?#xff0c;如果另外一個(gè)線程正在過(guò)時(shí)的鏈中查找那個(gè)被刪除的元素,它會(huì)立即看到一個(gè)空值,并知道使用同步重新進(jìn)行檢索。最終,原始 hash 鏈中被刪除的元素將會(huì)被垃圾收集。
清單3. ConcurrentHashMap.remove() 實(shí)現(xiàn)
| protected Object remove(Object key, Object value) { /* Find the entry, then 1. Set value field to null, to force get() to retry 2. Rebuild the list without this entry. All entries following removed node can stay in list, but all preceding ones need to be cloned. Traversals rely on this strategy to ensure that elements will not be repeated during iteration. */ int hash = hash(key); Segment seg = segments[hash & SEGMENT_MASK]; synchronized(seg) { Entry[] tab = table; int index = hash & (tab.length-1); Entry first = tab[index]; Entry e = first; for (;;) { if (e == null) return null; if (e.hash == hash && eq(key, e.key)) break; e = e.next; } Object oldValue = e.value; if (value != null && !value.equals(oldValue)) return null; e.value = null; Entry head = e.next; for (Entry p = first; p != e; p = p.next) head = new Entry(p.hash, p.key, p.value, head); tab[index] = head; seg.count--; return oldValue; } } |
圖1為刪除一個(gè)元素之前的 hash 鏈:
圖1. Hash鏈
圖2為刪除元素3之后的鏈:
圖2. 一個(gè)元素的刪除過(guò)程
插入和更新操作
put()?的實(shí)現(xiàn)很簡(jiǎn)單。像?remove()?一樣,put()?會(huì)在執(zhí)行期間保持 bucket 鎖,但是由于?put()?并不是都需要獲取鎖,所以這并不一定會(huì)阻塞其他讀線程的執(zhí)行(也不會(huì)阻塞其他寫(xiě)線程訪問(wèn)別的 bucket)。它首先會(huì)在適當(dāng)?shù)?hash 鏈中搜索需要的鍵值。如果能夠找到,value字段(易變的)就直接被更新。如果沒(méi)有找到,新會(huì)創(chuàng)建一個(gè)用于描述新 map 的新?Entry?對(duì)象,然后插入到 bucket 列表的頭部。
弱一致的迭代器
由?ConcurrentHashMap?返回的迭代器的語(yǔ)義又不同于?ava.util?集合中的迭代器;而且它又是弱一致的(weakly consistent)而非?fail-fast?的(所謂 fail-fast 是指,當(dāng)正在使用一個(gè)迭代器的時(shí)候,如何底層的集合被修改,就會(huì)拋出一個(gè)異常)。當(dāng)一個(gè)用戶調(diào)用?keySet().iterator()?去迭代器中檢索一組 hash 鍵的時(shí)候,實(shí)現(xiàn)就簡(jiǎn)單地使用同步來(lái)保證每個(gè)鏈的頭指針是當(dāng)前值。next()和?hasNext()?操作以一種明顯的方式定義,即遍歷每個(gè)鏈然后轉(zhuǎn)到下一個(gè)鏈直到所有的鏈都被遍歷。弱一致迭代器可能會(huì)也可能不會(huì)反映迭代器迭代過(guò)程中的插入操作,但是一定會(huì)反映迭代器還沒(méi)有到達(dá)的鍵的更新或刪除操作,并且對(duì)任何值最多返回一次。ConcurrentHashMap返回的迭代器不會(huì)拋出?ConcurrentModificationException?異常。
動(dòng)態(tài)調(diào)整大小
隨著 map 中元素?cái)?shù)目的增長(zhǎng),hash 鏈將會(huì)變長(zhǎng),因此檢索時(shí)間也會(huì)增加。從某種意義上說(shuō),增加 bucket 的數(shù)目和重排其中的值是非常重要的。在有些像?Hashtable?之類(lèi)的類(lèi)中,這很簡(jiǎn)單,因?yàn)楸3忠粋€(gè)應(yīng)用到整個(gè) map 的獨(dú)占鎖是可能的。在?ConcurrentHashMap?中,每 次一個(gè)條目插入的時(shí)候,如果鏈的長(zhǎng)度超過(guò)了某個(gè)閾值,鏈就被標(biāo)記為需要調(diào)整大小。當(dāng)有足夠多的鏈被標(biāo)記為需要調(diào)整大小以后,ConcurrentHashMap?就使用遞歸獲取每個(gè) bucket 上的鎖并重排每個(gè) bucket 中的元素到一個(gè)新的 、更大的 hash 表中。多數(shù)情況下,這是自動(dòng)發(fā)生的,并且對(duì)調(diào)用者透明。
不鎖定?
要說(shuō)不用鎖定就可以成功地完成?get()?操作似乎有點(diǎn)言過(guò)其實(shí),因?yàn)?span style="font-family:'新宋體';">?Entry?的?value?字段是易變的,這是用來(lái)檢測(cè)更新和刪除的。在機(jī)器級(jí),易變的和同步的內(nèi)容通常在最后會(huì)被翻譯成相同的緩存一致原語(yǔ),所以這里會(huì)有一些?鎖定,雖然只是細(xì)粒度的并且沒(méi)有調(diào)度,或者沒(méi)有獲取和釋放監(jiān)視器的 JVM 開(kāi)銷(xiāo)。但是,除語(yǔ)義之外,在很多通用的情況下,檢索的次數(shù)大于插入和刪除的次數(shù),所以說(shuō)由?ConcurrentHashMap?取得的并發(fā)性是相當(dāng)高的。
結(jié)束語(yǔ)
ConcurrentHashMap?對(duì)于很多并發(fā)應(yīng)用程序來(lái)說(shuō)是一個(gè)非常有用的類(lèi),而且對(duì)于理解 JMM 何以取得較高性能的微妙細(xì)節(jié)是一個(gè)很好的例子。ConcurrentHashMap?是編碼的經(jīng)典,需要深刻理解并發(fā)和 JMM 才能夠?qū)懙贸觥J褂盟?#xff0c;從中學(xué)到東西,享受其中的樂(lè)趣——但是除非您是Java 并發(fā)方面的專(zhuān)家,否則的話您自己不應(yīng)該這樣試。
轉(zhuǎn)載于:https://www.cnblogs.com/suifengbingzhu/archive/2012/08/27/2658369.html
總結(jié)
以上是生活随笔為你收集整理的ConcurrentHashMap,一个更快的HashMap的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Yii 2 的安装 之 踩坑历程
- 下一篇: 转: telnet命令学习