ConcurrentHashMap 是 HashMap 的線程安全版本,與之前版本的ConcurrentHashMap實現來看,java 8中做了較大調整,本文僅分析java 8的實現,java 8 之前的實現暫不做分析。
簡單介紹
爲了更好的導入本文,首先展示一下ConcurrentHashMap的結構,請看下面的圖片:
和 HashMap 一樣,ConcurrentHashMap 使用了一個 table 來存儲 Node,ConcurrentHashMap 同樣使用記錄的 key 的 hashCode來尋找記錄的存儲 index,而處理哈希衝突的方式與 HashMap 也是類似的,衝突的記錄將被存儲在同一位置上,形成一條鏈表,當鏈表長度大於 8 時,會將鏈表轉化爲紅黑樹。從而將查找的複雜度從 O(N) 降到 O(logN)。
ConcurrentHashMap 怎麼保證線程安全?
我們通過 ConcurrentHashMap 的 put 方法來講解 它怎麼實現線程安全。
put 方法實現步驟:
- 計算記錄的 key 的 hashCode ,然後計算 table 的 index 位置,獲取該 index 的值 x;
- 如果 x 爲 null,說明還沒有記錄,調用 CAS 操作 該新的記錄插入到table的index位置上去;
- 如果 x 不爲 null,通過 synchronized 關鍵字 對 table 的 index 位置加鎖;
- 然後判斷table的index位置上的第一個節點的hashCode值,如果hashCode值小於0,那麼就是一顆紅黑樹,如果不小於0,那麼就還是一條鏈表;
- 如果是一條鏈表,查找鏈表尋找是否有一個記錄的 key 值和本次插入的 key 值相同,相同則將替換掉 value 值;不同的話直接添加到鏈表中;
- 如果是一棵紅黑樹,調用 putTreeVal方法進行插入操作;
- 插入完成,判斷是不是更新操作,不是更新操作的話,size + 1。
注意:
- 第 2 步中,CAS操作 即compare and swap,是非阻塞的原子性操作,由UnSafe類提供。
- 第 3 步中,當前線程只會鎖住table的index位置,其他位置上沒有鎖住,所以此時其他線程可以安全的獲得其他的table位置來進行操作。這也就提高了ConcurrentHashMap的併發度。
- 第 7 步中,table的擴容操作也會在更新size的時候發生,如果在更新size之後發現table中的記錄數量達到了閾值,就需要進行擴容操作。
總結:
Java8 之後,ConcurrentHashMap 底層數據結構跟 HashMap 類似,都是基於 數組+鏈表+紅黑樹實現的,而 ConcurrentHashMap 之所以是線程安全的,是因爲它拋棄了 Hashtable 給整個方法加 synchronized 鎖的理念,而是讓 CAS操作 和 synchronized鎖 結合,在進行修改操作是,只對 table數組 的一項進行加鎖,其他數組項可以繼續被其他線程使用,這就提高了 ConcurrentHashMap 的併發度。
--------------------------------------------- 想看 put方法 的源碼?請往下看 ---------------------------------------------
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();//ConcurrentHashMap不存儲key/value爲null
int hash = spread(key.hashCode());//計算key的hash值
int binCount = 0;//桶中元素的大小,如果大於等於8,則旋轉爲紅黑樹
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;//f :計算出key在 hash中數組桶的鏈表/紅黑樹的根,n:桶的長度,i:key在數組的位置,fh:f的hash值
if (tab == null || (n = tab.length) == 0)
tab = initTable();//如果數組爲0或者沒有元素,初始化
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//f指向數組中的值(鏈表/紅黑樹)
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))//如果f爲 null表示鏈表沒有值,則此次放入的key/value存入鏈表的根 (1)
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)//如果在進行擴容先進行擴容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {//此次添加key/value的鏈表有元素,則對鏈表/紅黑樹進行加鎖 這裏是java8和java7的不同之處 java8採用的加鎖桶,而不是一段桶
if (tabAt(tab, i) == f) {//出現hash碰撞(情況1:線程1和線程2同時插入在上面(1) 由於是CAS操作只有一個線程會成功,第二個線程會進入到這一步 情況2:普通的hash碰撞),
if (fh >= 0) {//大於0表示桶是鏈表 TREEBIN = -2 桶是紅黑樹
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;//鏈表中的一個元素的key
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {//如何hash和key都和已存在的元素相等則根據onlyIfAbsebt的值,確定是用之前的值還是新值覆蓋
oldVal = e.val;
if (!onlyIfAbsent)//如果onlyIfAbsent爲fasle,新值覆蓋老值
e.val = value;
break;//退出,操作完成
}
Node<K,V> pred = e;//鏈表最末尾的值作爲新值的前一個元素
if ((e = e.next) == null) {//如果已經到了末尾值,則創建新的node存放此次插入的key/value
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;
}
}
}
}
if (binCount != 0) {//如果不等於0判斷是否需要旋轉爲紅黑樹
if (binCount >= TREEIFY_THRESHOLD)//如果大於8則旋轉爲紅黑樹
treeifyBin(tab, i);//旋轉爲紅黑樹
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
明天的你,一定會感謝今天努力的自己!