java ConcurrentHashMap 实现原理
由于?HashMap?是一個(gè)線程不安全的容器,主要體現(xiàn)在容量大于總量*負(fù)載因子發(fā)生擴(kuò)容時(shí)會(huì)出現(xiàn)環(huán)形鏈表從而導(dǎo)致死循環(huán)。
因此需要支持線程安全的并發(fā)容器?ConcurrentHashMap?。
JDK1.7 實(shí)現(xiàn)
數(shù)據(jù)結(jié)構(gòu)
如圖所示,是由?Segment?數(shù)組、HashEntry?數(shù)組組成,和?HashMap?一樣,仍然是數(shù)組加鏈表組成。
ConcurrentHashMap?采用了分段鎖技術(shù),其中?Segment?繼承于?ReentrantLock。不會(huì)像?HashTable?那樣不管是?put?還是?get?操作都需要做同步處理,理論上 ConcurrentHashMap 支持?CurrencyLevel?(Segment 數(shù)組數(shù)量)的線程并發(fā)。每當(dāng)一個(gè)線程占用鎖訪問(wèn)一個(gè)?Segment?時(shí),不會(huì)影響到其他的?Segment。
get 方法
ConcurrentHashMap?的?get?方法是非常高效的,因?yàn)檎麄€(gè)過(guò)程都不需要加鎖。
只需要將?Key?通過(guò)?Hash?之后定位到具體的?Segment?,再通過(guò)一次?Hash?定位到具體的元素上。由于?HashEntry?中的?value?屬性是用?volatile?關(guān)鍵詞修飾的,保證了內(nèi)存可見性,所以每次獲取時(shí)都是最新值(volatile 相關(guān)知識(shí)點(diǎn))。
put 方法
內(nèi)部?HashEntry?類 :
static final class HashEntry<K,V> {final int hash;final K key;volatile V value;volatile HashEntry<K,V> next;HashEntry(int hash, K key, V value, HashEntry<K,V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}}雖然 HashEntry 中的 value 是用?volatile?關(guān)鍵詞修飾的,但是并不能保證并發(fā)的原子性,所以 put 操作時(shí)仍然需要加鎖處理。
首先也是通過(guò) Key 的 Hash 定位到具體的 Segment,在 put 之前會(huì)進(jìn)行一次擴(kuò)容校驗(yàn)。這里比 HashMap 要好的一點(diǎn)是:HashMap 是插入元素之后再看是否需要擴(kuò)容,有可能擴(kuò)容之后后續(xù)就沒有插入就浪費(fèi)了本次擴(kuò)容(擴(kuò)容非常消耗性能)。
而 ConcurrentHashMap 不一樣,它是在將數(shù)據(jù)插入之前檢查是否需要擴(kuò)容,之后再做插入操作。
size 方法
每個(gè)?Segment?都有一個(gè)?volatile?修飾的全局變量?count?,求整個(gè)?ConcurrentHashMap?的?size?時(shí)很明顯就是將所有的?count?累加即可。但是?volatile?修飾的變量卻不能保證多線程的原子性,所有直接累加很容易出現(xiàn)并發(fā)問(wèn)題。
但如果每次調(diào)用?size?方法將其余的修改操作加鎖效率也很低。所以做法是先嘗試兩次將?count?累加,如果容器的?count?發(fā)生了變化再加鎖來(lái)統(tǒng)計(jì)?size。
至于?ConcurrentHashMap?是如何知道在統(tǒng)計(jì)時(shí)大小發(fā)生了變化呢,每個(gè)?Segment?都有一個(gè)?modCount?變量,每當(dāng)進(jìn)行一次?put remove?等操作,modCount?將會(huì) +1。只要?modCount?發(fā)生了變化就認(rèn)為容器的大小也在發(fā)生變化。
JDK1.8 實(shí)現(xiàn)
1.8 中的 ConcurrentHashMap 數(shù)據(jù)結(jié)構(gòu)和實(shí)現(xiàn)與 1.7 還是有著明顯的差異。
其中拋棄了原有的 Segment 分段鎖,而采用了?CAS + synchronized?來(lái)保證并發(fā)安全性。
也將 1.7 中存放數(shù)據(jù)的 HashEntry 改為 Node,但作用都是相同的。
其中的?val next?都用了 volatile 修飾,保證了可見性。
put 方法
重點(diǎn)來(lái)看看 put 函數(shù):
- 根據(jù) key 計(jì)算出 hashcode 。
- 判斷是否需要進(jìn)行初始化。
- f?即為當(dāng)前 key 定位出的 Node,如果為空表示當(dāng)前位置可以寫入數(shù)據(jù),利用 CAS 嘗試寫入,失敗則自旋保證成功。
- 如果當(dāng)前位置的?hashcode == MOVED == -1,則需要進(jìn)行擴(kuò)容。
- 如果都不滿足,則利用 synchronized 鎖寫入數(shù)據(jù)。
- 如果數(shù)量大于?TREEIFY_THRESHOLD?則要轉(zhuǎn)換為紅黑樹。
get 方法
- 根據(jù)計(jì)算出來(lái)的 hashcode 尋址,如果就在桶上那么直接返回值。
- 如果是紅黑樹那就按照樹的方式獲取值。
- 都不滿足那就按照鏈表的方式遍歷獲取值。
總結(jié)
1.8 在 1.7 的數(shù)據(jù)結(jié)構(gòu)上做了大的改動(dòng),采用紅黑樹之后可以保證查詢效率(O(logn)),甚至取消了 ReentrantLock 改為了 synchronized,這樣可以看出在新版的 JDK 中對(duì) synchronized 優(yōu)化是很到位的。
總結(jié)
以上是生活随笔為你收集整理的java ConcurrentHashMap 实现原理的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: java ReentrantLock 实
- 下一篇: 如何成为一位「不那么差」的程序员