今天來介紹大名鼎鼎的ConcurrentHashMap,衆所周知,Java.Utils.Concurrent包出現後,就立馬成爲高併發的利器,而靠一己之力把此包寫出來的Doug Lea,則更是高併發大神。此篇文章僅僅限於描述ConcurrentHashMap冰山一角,並不能對其全面剖析,若是有讀者想要對併發進行更深刻的理解與交流,推薦《Java併發編程的藝術》,筆者看完頗有感悟。node
此文仍是從最簡單也是最經常使用的get,put方法來進行剖析,進而逐步抽絲剝繭,分析此類的全貌。理解此文章須要讀者有HashMap的基礎,且建議讀者在閱讀此文章時,腦中必定要兩個甚至多個線程的概念,切勿以單線程模型來思考。 從註釋能夠得知全部參數都不能夠爲null. 與HashMap不一樣數據庫
All arguments to all task methods must be non-null
複製代碼
/**
* Maps the specified key to the specified value in this table.
* Neither the key nor the value can be null.
*
* <p>The value can be retrieved by calling the {@code get} method
* with a key that is equal to the original key.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with {@code key}, or
* {@code null} if there was no mapping for {@code key}
* @throws NullPointerException if the specified key or value is null
*/
public V put(K key, V value) {
return putVal(key, value, false);
}
複製代碼
能夠看到put函數只是簡單調用了putVal這個函數,那麼繼續往下編程
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();//參數檢測異常
int hash = spread(key.hashCode()); //Rehash
int binCount = 0;
for (Node<K,V>[] tab = table;;) {//死循環
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();//第一次進來,初始化table
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//1 此位置上尚未元素插入,則利用cas鎖,
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)//2
tab = helpTransfer(tab, f);
else { // 3
V oldVal = null;
synchronized (f) {//3.1
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {//3.2
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
複製代碼
由上圖源碼可知,死循環+四個if else就是整個put函數的核心實現。四個if else分別對應以下功能數組
若是table爲空,則初始化table(能夠理解爲一個數組,其實此處初始化也別有洞天,由於要防止多個線程同時初始化,有興趣的讀者能夠本身去研究一下,看看Doug Lea是用了什麼方式,防止table被屢次初始化)安全
註釋1處的,若是數組對應的索引位置處尚未元素,則利用casTabAt進行放置key,value 此處主要有兩點須要注意: 1 tabAt是利用的unSafe類裏的getObjectVolatile(),熟悉Volatile關鍵字的同窗確定知道,這是獲取該對象在內存中最新的值。(即同時有多個線程修改此變量,則JVM happen-before原則可以幫助咱們獲取到最新的值)。該對象便是table對應索引處的node對象。bash
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
複製代碼
2 若是casTabAt成功,則put成功,break掉死循環。 casTabAt()也是利用了UnSafe類裏的compareAndSwapObject函數,關於此類能夠多說幾句,此類是Java用來利用CAS鎖機制而現實的一個接口類。(各位同窗必定要弄明白,CAS鎖其實不須要上層作任何操做,它的可靠性是由底層硬件指令來保證的,上層只是調用),返回一個boolean。即若是插入失敗,則重試。爲何會插入失敗,各位能夠思考一下。併發
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
複製代碼
3 註釋2處,此處判斷是否正處於轉移中,若是要插入的位置,正在轉移,也就是整個table正處於擴容階段,則幫助其轉移。app
4 註釋3處,Put函數的核心。若是走到此分支,則證實函數
到此整個ConcurrentHashMap的Put函數分析就結束了,是否是很簡單呢?那是由於咱們沒有分析put函數裏兩個較爲硬核的addCount()與helpTransfer()函數。高併發
總結 ConcurrentHashMap put函數的精髓就在於利用CAS替換所在位置,與鎖住鏈表表頭(或者是紅黑樹的root節點),進行修改。若是有同窗接觸過數據庫,則會聯繫到此實現相似於數據庫的行級鎖。其優勢是,下降的鎖的粒度,提升了併發的效率。其缺點 則是非絕對的線程安全。
「當多個線程訪問某個類時,無論運行時環境採用何種調度方式或者這些線程將如何交替進行,而且在主調
代碼中不須要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼稱這個類是絕對的線程安全的。」
複製代碼
接下來繼續看Get函數。
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code key.equals(k)},
* then this method returns {@code v}; otherwise it returns
* {@code null}. (There can be at most one such mapping.)
*
* @throws NullPointerException if the specified key is null
*/
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)//註釋1
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
複製代碼
額,到這裏感受沒什麼可講的,由於讀模式的話基本不涉及到線程變量衝突的問題,因此你們也能夠看到get函數跟hashmap的get函數相差並不大。 也是簡單的條件判斷,而後查詢key對應的node是否存在。 稍微有點區別的就是註釋1處了。