轉(zhuǎn):http:///java-concurrent-hashmap-2/ 經(jīng)過之前的鋪墊,現(xiàn)在可以進(jìn)入正題了。 對于哈希表,Java中采用鏈表的方式來解決hash沖突的。 實(shí)現(xiàn)了同步的HashTable也是這樣的結(jié)構(gòu),它的同步使用鎖來保證的,并且所有同步操作使用的是同一個(gè)鎖對象。這樣若有n個(gè)線程同時(shí)在get時(shí),這n個(gè)線程要串行的等待來獲取鎖。
ConcurrentHashMap中對這個(gè)數(shù)據(jù)結(jié)構(gòu),針對并發(fā)稍微做了一點(diǎn)調(diào)整。 創(chuàng)建好默認(rèn)的ConcurrentHashMap之后,它的結(jié)構(gòu)大致如下圖: 看起來只是把以前HashTable的一個(gè)hash bucket創(chuàng)建了16份而已。有什么特別的嗎?沒啥特別的。 繼續(xù)看每個(gè)segment是怎么定義的:
Segment繼承了ReentrantLock,表明每個(gè)segment都可以當(dāng)做一個(gè)鎖。(ReentrantLock前文已經(jīng)提到,不了解的話就把當(dāng)做synchronized的替代者吧)這樣對每個(gè)segment中的數(shù)據(jù)需要同步操作的話都是使用每個(gè)segment容器對象自身的鎖來實(shí)現(xiàn)。只有對全局需要改變時(shí)鎖定的是所有的segment。 上面的這種做法,就稱之為“分離鎖(lock striping)”。有必要對“分拆鎖”和“分離鎖”的概念描述一下:
看上去,單是這樣就已經(jīng)能大大提高多線程并發(fā)的性能了。還沒完,繼續(xù)看我們關(guān)注的get,put,remove這三個(gè)函數(shù)怎么保證數(shù)據(jù)同步的。 先看get方法:
它沒有使用同步控制,交給segment去找,再看Segment中的get方法:
它也沒有使用鎖來同步,只是判斷獲取的entry的value是否為null,為null時(shí)才使用加鎖的方式再次去獲取。 這個(gè)實(shí)現(xiàn)很微妙,沒有鎖同步的話,靠什么保證同步呢?我們一步步分析。 第一步,先判斷一下 count != 0;count變量表示segment中存在entry的個(gè)數(shù)。如果為0就不用找了。 第二步,獲取到要該key所在segment中的索引地址,如果該地址有相同的hash對象,順著鏈表一直比較下去找到該entry。當(dāng)找到entry的時(shí)候,先做了一次比較: 考慮一下,如果這個(gè)時(shí)候,另一個(gè)線程恰好新增/刪除了entry,或者改變了entry的value,會(huì)如何? 先看一下HashEntry類結(jié)構(gòu)。
除了 value,其它成員都是final修飾的,也就是說value可以被改變,其它都不可以改變,包括指向下一個(gè)HashEntry的next也不能被改變。(那刪除一個(gè)entry時(shí)怎么辦?后續(xù)會(huì)講到。) 1) 在get代碼的①和②之間,另一個(gè)線程新增了一個(gè)entry 下圖大致描述了put 一個(gè)新的entry的過程。 因?yàn)槊總€(gè)HashEntry中的next也是final的,沒法對鏈表最后一個(gè)元素增加一個(gè)后續(xù)entry newEntry對象是通過 回想一下我們之前討論的DCL的問題,這里也一樣,沒有鎖同步的話,new 一個(gè)對象對于多線程看到這個(gè)對象的狀態(tài)是沒有保障的,這里同樣有可能一個(gè)線程new這個(gè)對象的時(shí)候還沒有執(zhí)行完構(gòu)造函數(shù)就被另一個(gè)線程得到這個(gè)對象引用。 有沒有可能會(huì)put進(jìn)一個(gè)value為null的entry? 不會(huì)的,已經(jīng)做了檢查,這種情況會(huì)拋出異常,所以 ②處的判斷完全是出于對多線程下訪問一個(gè)new出來的對象的狀態(tài)檢測。 2) 在get代碼的①和②之間,另一個(gè)線程修改了一個(gè)entry的value 3) 在get代碼的①之后,另一個(gè)線程刪除了一個(gè)entry 假設(shè)我們的鏈表元素是:e1-> e2 -> e3 -> e4 我們要?jiǎng)h除 e3這個(gè)entry 如果我們get的也恰巧是e3,可能我們順著鏈表剛找到e1,這時(shí)另一個(gè)線程就執(zhí)行了刪除e3的操作,而我們線程還會(huì)繼續(xù)沿著舊的鏈表找到e3返回。 我們第①處就判斷了count變量,它保障了在 ①處能看到其他線程修改后的。 不過這也沒什么關(guān)系,即使我們返回e3的時(shí)候,它被其他線程刪除了,暴漏出去的e3也不會(huì)對我們新的鏈表造成影響。 這其實(shí)是一種樂觀設(shè)計(jì),設(shè)計(jì)者假設(shè) ①之后到②之間 發(fā)生被其它線程增、刪、改的操作可能性很小,所以不采用同步設(shè)計(jì),而是采用了事后(其它線程這期間也來操作,并且可能發(fā)生非安全事件)彌補(bǔ)的方式。 這樣做減少了使用互斥鎖對并發(fā)性能的影響??赡苡腥藨岩蓃emove操作中復(fù)制鏈表的方式是否代價(jià)太大,這里我沒有深入比較,不過既然Java5中這么實(shí)現(xiàn),我想new一個(gè)對象的代價(jià)應(yīng)該已經(jīng)沒有早期認(rèn)為的那么嚴(yán)重。 我們基本分析完了get操作。對于put和remove操作,是使用鎖同步來進(jìn)行的,不過是用的ReentrantLock而不是synchronized,性能上要更高一些。它們的實(shí)現(xiàn)前文都已經(jīng)提到過,就沒什么可分析的了。 我們還需要知道一點(diǎn),ConcurrentHashMap的迭代器不是Fast-Fail的方式,所以在迭代的過程中別其他線程添加/刪除了元素,不會(huì)拋出異常,也不能體現(xiàn)出元素的改動(dòng)。但也沒有關(guān)系,因?yàn)槊總€(gè)entry的成員除了value都是final修飾的,暴漏出去也不會(huì)對其他元素造成影響。 最后,再來看一下Java6文檔中對ConcurrentHashMap的描述(我們分析過的地方或者需要注意的使用了紅色字體):
參考: http://www./topic/344876 本來我的分析已經(jīng)結(jié)束,不過正好看到Concurrency-interest郵件組中的一個(gè)問題,可以加深一下我們隊(duì)ConcurrentHashMap的理解,同時(shí)也反駁了我剛開始所說的“ConcurrentHashMap完全可以替代HashTable”,我必須糾正一下。先看例子:
I would expect that one of the following scenarios to be true (for the contents of the map) after this code runs:
However, upon inspection of ConcurrentHashMap, it seems to me that the following scenario might also be true:
This seems surprising because “first” gets put before “second”, so if “second” is cleared, then surely “first” should be cleared too. Likewise, consider the following code:
I would expect one of the following scenarios to be true for the contents of myKeys after this code has run:
However, upon closer inspection, it seems that the following scenario might also be true:
This is surprising because “second” is only ever put into the map after “first” is removed. They should never be in the map simultaneously, but an iterator might perceive them to be so. 對于這兩個(gè)現(xiàn)象的解釋:
如果線程b先執(zhí)行了clear,清空了一部分segment的時(shí)候,線程a執(zhí)行了put且正好把“first”放入了“清空過”的segment中,而把“second”放到了還沒有清空過的segment中,就會(huì)出現(xiàn)上面的情況。 第二段代碼,如果線程b執(zhí)行了迭代遍歷到first,而此時(shí)線程a還沒有remove掉first,那么即使后續(xù)刪除了first,迭代器里不會(huì)反應(yīng)出來,也不拋出異常,這種迭代器被稱為“弱一致性”(weakly consistent)迭代器。 Doug Lea 對這個(gè)問題的回復(fù)中提到:
大意是我們將“一致性強(qiáng)度”和“擴(kuò)展性”之間的對比交給用戶來權(quán)衡,所以大多數(shù)集合都提供了synchronized和concurrent兩個(gè)版本。 通過他的說法,我必須糾正我一開始以為ConcurrentHashMap完全可以代替HashTable的說法,如果你的環(huán)境要求“強(qiáng)一致性”的話,就不能用ConcurrentHashMap了,它的get,clear方法和迭代器都是“弱一致性”的。不過真正需要“強(qiáng)一致性”的場景可能非常少,我們大多應(yīng)用中ConcurrentHashMap是滿足的。 (全文完)如果您喜歡此文請點(diǎn)贊,分享,評論。 |
|