2021-03-14 分類: 網(wǎng)站建設(shè)
ConcurrentHashMap 和 HashMap 思路是差不多的,但是因?yàn)樗С植l(fā)操作,所以要復(fù)雜一些。
整個 ConcurrentHashMap 由一個個 Segment 組成,Segment 代表”部分“或”一段“的意思,所以很多地方都會將其描述為分段鎖。注意,行文中,我很多地方用了“槽”來代表一個 segment。
簡單理解就是,ConcurrentHashMap 是一個 Segment 數(shù)組,Segment 通過繼承 ReentrantLock 來進(jìn)行加鎖,所以每次需要加鎖的操作鎖住的是一個 segment,這樣只要保證每個 Segment 是線程安全的,也就實(shí)現(xiàn)了全局的線程安全。
concurrencyLevel:并行級別、并發(fā)數(shù)、Segment 數(shù),怎么翻譯不重要,理解它。默認(rèn)是 16,也就是說 ConcurrentHashMap 有 16 個 Segments,所以理論上,這個時候,最多可以同時支持 16 個線程并發(fā)寫,只要它們的操作分別分布在不同的 Segment 上。這個值可以在初始化的時候設(shè)置為其他值,但是一旦初始化以后,它是不可以擴(kuò)容的。
再具體到每個 Segment 內(nèi)部,其實(shí)每個 Segment 很像之前介紹的 HashMap,不過它要保證線程安全,所以處理起來要麻煩些。
initialCapacity:初始容量,這個值指的是整個 ConcurrentHashMap 的初始容量,實(shí)際操作的時候需要平均分給每個 Segment。
loadFactor:負(fù)載因子,之前我們說了,Segment 數(shù)組不可以擴(kuò)容,所以這個負(fù)載因子是給每個 Segment 內(nèi)部使用的。
初始化完成,我們得到了一個 Segment 數(shù)組。
我們就當(dāng)是用 new ConcurrentHashMap() 無參構(gòu)造函數(shù)進(jìn)行初始化的,那么初始化完成后:
我們先看 put 的主流程,對于其中的一些關(guān)鍵細(xì)節(jié)操作,后面會進(jìn)行詳細(xì)介紹。
第一層皮很簡單,根據(jù) hash 值很快就能找到相應(yīng)的 Segment,之后就是 Segment 內(nèi)部的 put 操作了。
Segment 內(nèi)部是由 數(shù)組+鏈表 組成的。
整體流程還是比較簡單的,由于有獨(dú)占鎖的保護(hù),所以 segment 內(nèi)部的操作并不復(fù)雜。至于這里面的并發(fā)問題,我們稍后再進(jìn)行介紹。
到這里 put 操作就結(jié)束了,接下來,我們說一說其中幾步關(guān)鍵的操作。
ConcurrentHashMap 初始化的時候會初始化第一個槽 segment[0],對于其他槽來說,在插入第一個值的時候進(jìn)行初始化。
這里需要考慮并發(fā),因?yàn)楹芸赡軙卸鄠€線程同時進(jìn)來初始化同一個槽 segment[k],不過只要有一個成功了就可以。
總的來說,ensureSegment(int k) 比較簡單,對于并發(fā)操作使用 CAS 進(jìn)行控制。
我沒搞懂這里為什么要搞一個 while 循環(huán),CAS 失敗不就代表有其他線程成功了嗎,為什么要再進(jìn)行判斷?
前面我們看到,在往某個 segment 中 put 的時候,首先會調(diào)用 node = tryLock() ? null : scanAndLockForPut(key, hash, value),也就是說先進(jìn)行一次 tryLock() 快速獲取該 segment 的獨(dú)占鎖,如果失敗,那么進(jìn)入到 scanAndLockForPut 這個方法來獲取鎖。
下面我們來具體分析這個方法中是怎么控制加鎖的。
這個方法有兩個出口,一個是 tryLock() 成功了,循環(huán)終止,另一個就是重試次數(shù)超過了 MAX_SCAN_RETRIES,進(jìn)到 lock() 方法,此方法會阻塞等待,直到成功拿到獨(dú)占鎖。
這個方法就是看似復(fù)雜,但是其實(shí)就是做了一件事,那就是獲取該 segment 的獨(dú)占鎖,如果需要的話順便實(shí)例化了一下 node。
重復(fù)一下,segment 數(shù)組不能擴(kuò)容,擴(kuò)容是 segment 數(shù)組某個位置內(nèi)部的數(shù)組 HashEntry\[] 進(jìn)行擴(kuò)容,擴(kuò)容后,容量為原來的 2 倍。
首先,我們要回顧一下觸發(fā)擴(kuò)容的地方,put 的時候,如果判斷該值的插入會導(dǎo)致該 segment 的元素個數(shù)超過閾值,那么先進(jìn)行擴(kuò)容,再插值,讀者這個時候可以回去 put 方法看一眼。
該方法不需要考慮并發(fā),因?yàn)榈竭@里的時候,是持有該 segment 的獨(dú)占鎖的。
這里的擴(kuò)容比之前的 HashMap 要復(fù)雜一些,代碼難懂一點(diǎn)。上面有兩個挨著的 for 循環(huán),第一個 for 有什么用呢?
仔細(xì)一看發(fā)現(xiàn),如果沒有第一個 for 循環(huán),也是可以工作的,但是,這個 for 循環(huán)下來,如果 lastRun 的后面還有比較多的節(jié)點(diǎn),那么這次就是值得的。因?yàn)槲覀冎恍枰寺?lastRun 前面的節(jié)點(diǎn),后面的一串節(jié)點(diǎn)跟著 lastRun 走就是了,不需要做任何操作。
我覺得 Doug Lea 的這個想法也是挺有意思的,不過比較壞的情況就是每次 lastRun 都是鏈表的最后一個元素或者很靠后的元素,那么這次遍歷就有點(diǎn)浪費(fèi)了。不過 Doug Lea 也說了,根據(jù)統(tǒng)計,如果使用默認(rèn)的閾值,大約只有 1/6 的節(jié)點(diǎn)需要克隆。
相對于 put 來說,get 真的不要太簡單。
現(xiàn)在我們已經(jīng)說完了 put 過程和 get 過程,我們可以看到 get 過程中是沒有加鎖的,那自然我們就需要去考慮并發(fā)問題。
添加節(jié)點(diǎn)的操作 put 和刪除節(jié)點(diǎn)的操作 remove 都是要加 segment 上的獨(dú)占鎖的,所以它們之間自然不會有問題,我們需要考慮的問題就是 get 的時候在同一個 segment 中發(fā)生了 put 或 remove 操作。
remove 操作我們沒有分析源碼,所以這里說的讀者感興趣的話還是需要到源碼中去求實(shí)一下的。
get 操作需要遍歷鏈表,但是 remove 操作會”破壞”鏈表。
如果 remove 破壞的節(jié)點(diǎn) get 操作已經(jīng)過去了,那么這里不存在任何問題。
如果 remove 先破壞了一個節(jié)點(diǎn),分兩種情況考慮。 1、如果此節(jié)點(diǎn)是頭結(jié)點(diǎn),那么需要將頭結(jié)點(diǎn)的 next 設(shè)置為數(shù)組該位置的元素,table 雖然使用了 volatile 修飾,但是 volatile 并不能提供數(shù)組內(nèi)部操作的可見性保證,所以源碼中使用了 UNSAFE 來操作數(shù)組,請看方法 setEntryAt。2、如果要刪除的節(jié)點(diǎn)不是頭結(jié)點(diǎn),它會將要刪除節(jié)點(diǎn)的后繼節(jié)點(diǎn)接到前驅(qū)節(jié)點(diǎn)中,這里的并發(fā)保證就是 next 屬性是 volatile 的。
網(wǎng)站題目:concurrenthashmap你經(jīng)常用,但你可能并不了解
文章源于:http://chinadenli.net/news11/105161.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供Google、企業(yè)網(wǎng)站制作、外貿(mào)建站、微信小程序、網(wǎng)站建設(shè)、移動網(wǎng)站建設(shè)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)
猜你還喜歡下面的內(nèi)容