關於ConcurrentHashMap在以前的ConcurrentHashMap原理分析中已經解釋了原理,而HashTable其實大抵上只是對HashMap的線程安全的封裝,在JDK7與JDK8中HashMap的實現中解釋了HashMap的原理。java
至此你應該可以明白,ConcurrentHashMap與HashTable均可以用於多線程的環境,可是當Hashtable的大小增長到必定的時候,性能會急劇降低,由於迭代時須要被鎖定很長的時間。由於ConcurrentHashMap引入了分割(segmentation),不論它變得多麼大,僅僅須要鎖定map的某個部分,而其它的線程不須要等到迭代完成才能訪問map。簡而言之,在迭代的過程當中,ConcurrentHashMap僅僅鎖定map的某個部分,而Hashtable則會鎖定整個map。c++
那麼既然ConcurrentHashMap那麼優秀,爲何還要有Hashtable的存在呢?ConcurrentHashMap能徹底替代HashTable嗎?數組
HashTable雖然性能上不如ConcurrentHashMap,但並不能徹底被取代,二者的迭代器的一致性不一樣的,HashTable的迭代器是強一致性的,而ConcurrentHashMap是弱一致的。 ConcurrentHashMap的get,clear,iterator 都是弱一致性的。 Doug Lea 也將這個判斷留給用戶本身決定是否使用ConcurrentHashMap。安全
那麼什麼是強一致性和弱一致性呢?數據結構
get方法是弱一致的,是什麼含義?可能你指望往ConcurrentHashMap底層數據結構中加入一個元素後,立馬能對get可見,但ConcurrentHashMap並不能如你所願。換句話說,put操做將一個元素加入到底層數據結構後,get可能在某段時間內還看不到這個元素,若不考慮內存模型,單從代碼邏輯上來看,倒是應該能夠看獲得的。多線程
下面將結合代碼和java內存模型相關內容來分析下put/get方法。put方法咱們只需關注Segment#put,get方法只需關注Segment#get,在繼續以前,先要說明一下Segment裏有兩個volatile變量:count和table;HashEntry裏有一個volatile變量:value。app
Segment#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(); } }
Segment#getspa
V get(Object key, int hash) { if (count != 0) { // read-volatile HashEntry e = getFirst(hash); 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; } } return null; }
咱們如何肯定線程1放入某個變量的值是否對線程2可見?當a hb(happen before) c時,a對c可見,那麼咱們接下來咱們只要尋找put和get之間全部可能的執行軌跡上的hb關係。要找出hb關係,咱們須要先找出與hb相關的Action。爲方便,這裏將兩段代碼放到了一張圖片上。.net
能夠注意到,同一個Segment實例中的put操做是加了鎖的,而對應的get卻沒有。根據hb關係中的線程間Action類別,能夠從上圖中找出這些Action,主要是volatile讀寫和加解鎖,也就是圖中畫了橫線的那些。
put操做能夠分爲兩種狀況,一是key已經存在,修改對應的value;二是key不存在,將一個新的Entry加入底層數據結構。
key已經存在的狀況比較簡單,即if (e != null)部分,前面已經說過HashEntry的value是個volatile變量,當線程1給value賦值後,會立馬對執行get的線程2可見,而不用等到put方法結束。
key不存在的狀況稍微複雜一些,新加一個Entry的邏輯在else中。那麼將new HashEntry賦值給tab[index]是否能馬上對執行get的線程可見呢?咱們只需分析寫tab[index]與讀取tab[index]之間是否有hb關係便可。
假設執行put的線程與執行get的線程的軌跡是這樣的
執行put的線程 | 執行get的線程 |
⑧tab[index] = new HashEntry<K,V>(key, hash, first, value) | |
②count = c | |
③if (count != 0) | |
⑨HashEntry e = getFirst(hash); |
tab變量是一個普通的變量,雖然給它賦值的是volatile的table。另外,雖然引用類型(數組類型)的變量table是volatile的,但table中的元素不是volatile的,所以⑧只是一個普通的寫操做;count變量是volatile的,所以②是一個volatile寫;③很顯然是一個volatile讀;⑨中getFirst方法中讀取了table,所以包含一個volatile讀。
根據Synchronization Order,對同一個volatile變量,有volatile寫 hb volatile讀。在這個執行軌跡中,時間上②在③以前發生,且②是寫count,③是讀count,都是針對同一個volatile變量count,所以有② hb ③;又由於⑧和②是同一個線程中的,③和⑨是同一個線程中的,根據Program Order,有⑧ hb ②,③ hb ⑨。目前咱們有了三組關係了⑧ hb ②,② hb ③,③ hb ⑨,再根據hb關係是可傳遞的(即如有x hb y且y hb z,可得出x hb z),能夠得出⑧ hb ⑨。所以,若是按照上述執行軌跡,⑧中寫入的數組元素對⑨中的讀取操做是可見的。
再考慮這樣一個執行軌跡:
執行put的線程 | 執行get的線程 |
⑧tab[index] = new HashEntry<K,V>(key, hash, first, value) | |
③if (count != 0) | |
②count = c | |
⑨HashEntry e = getFirst(hash); |
這裏只是變換了下執行順序。每條語句的volatile讀寫含義同上,但它們之間的hb關係卻改變了。Program Order是咱們一直擁有的,即咱們有⑧ hb ②,③ hb ⑨。但此次對volatile的count的讀時間上發生在對count的寫以前,咱們沒法得出② hb ⑨這層關係了。所以,經過count變量,在這個軌跡上是沒法得出⑧ hb ⑨的。那麼,存不存在其它可替換關係,讓咱們仍能得出⑧ hb ⑨呢?
咱們要找的是,在⑧以後有一條語句或指令x,在⑨以前有一條語句或指令y,存在x hb y。這樣咱們能夠有⑧ hb x,x hb y, y hb ⑨。就讓咱們來找一下是否存在這樣的x和y。圖中的⑤、⑥、⑦、①存在volatile讀寫,可是它們在⑧以前,所以對確立⑧ hb ⑨這個關係沒有用處;同理,④在⑨以後,咱們要找的是⑨以前的,所以也對這個問題無益。前面已經分析過了②,③之間無法確立hb關係。
在⑧以後,咱們發現一個unlock操做,若是能在⑨以前找到一個lock操做,那麼咱們要找的x就是unlock,要找的y就是lock,由於Synchronization Order中有unlock hb lock的關係。可是,很不幸運,⑨以前沒有lock操做。所以,對於這樣的軌跡,是沒有⑧ hb ⑨關係的,也就是說,若是某個Segment實例中的put將一個Entry加入到了table中,在未執行count賦值操做以前有另外一個線程執行了同一個Segment實例中的get,來獲取這個剛加入的Entry中的value,那麼是有可能取不到的!
此外,若是getFirst(hash)先執行,tab[index] = new HashEntry<K,V>(key, hash, first, value)後執行,那麼,這個get操做也是看不到put的結果的。
……
正是由於get操做幾乎全部時候都是一個無鎖操做(get中有一個readValueUnderLock調用,不過這句執行到的概率極小),使得同一個Segment實例上的put和get能夠同時進行,這就是get操做是弱一致的根本緣由。Java API中對此有一句簡單的描述:
Retrievals reflect the results of the most recently completed update operations holding upon their onset.
也就是說API上保證get操做必定能看到已完成的put操做。已完成的put操做確定在get讀取count以前對count作了寫入操做。所以,也就是咱們第一個軌跡分析的狀況。
ConcurrentHashMap#clear
clear方法很簡單,看下代碼即知。
public void clear() { for (int i = 0; i < segments.length; ++i) segments[i].clear(); }
由於沒有全局的鎖,在清除完一個segments以後,正在清理下一個segments的時候,已經清理segments可能又被加入了數據,所以clear返回的時候,ConcurrentHashMap中是可能存在數據的。所以,clear方法是弱一致的。
ConcurrentHashMap中的迭代器
ConcurrentHashMap中的迭代器主要包括entrySet、keySet、values方法。它們大同小異,這裏選擇entrySet解釋。當咱們調用entrySet返回值的iterator方法時,返回的是EntryIterator,在EntryIterator上調用next方法時,最終實際調用到了HashIterator.advance()方法,看下這個方法:
final void advance() { if (nextEntry != null && (nextEntry = nextEntry.next) != null) return; while (nextTableIndex >= 0) { if ( (nextEntry = currentTable[nextTableIndex--]) != null) return; } while (nextSegmentIndex >= 0) { Segment<K,V> seg = segments[nextSegmentIndex--]; if (seg.count != 0) { currentTable = seg.table; for (int j = currentTable.length - 1; j >= 0; --j) { if ( (nextEntry = currentTable[j]) != null) { nextTableIndex = j - 1; return; } } } } }
這個方法在遍歷底層數組。在遍歷過程當中,若是已經遍歷的數組上的內容變化了,迭代器不會拋出ConcurrentModificationException異常。若是未遍歷的數組上的內容發生了變化,則有可能反映到迭代過程當中。這就是ConcurrentHashMap迭代器弱一致的表現。
總結
ConcurrentHashMap的弱一致性主要是爲了提高效率,是一致性與效率之間的一種權衡。要成爲強一致性,就獲得處使用鎖,甚至是全局鎖,這就與Hashtable和同步的HashMap同樣了。
1. http://blog.csdn.net/kobejayandy/article/details/16834311
2. http://ifeve.com/concurrenthashmap-vs-hashtable/
3. http://ifeve.com/concurrenthashmap-weakly-consistent/