jdk1.7分段鎖的實現
和hashmap一樣,在jdk1.7中ConcurrentHashMap的底層數據結構是數組加鏈表。和hashmap不同的是ConcurrentHashMap中存放的數據是一段段的,即由多個Segment(段)組成的。每個Segment中都有着類似於數組加鏈表的結構。
關於Segment
ConcurrentHashMap有3個參數:
initialCapacity
:初始總容量,默認16loadFactor
:加載因子,默認0.75concurrencyLevel
:併發級別,默認16
其中併發級別控制了Segment的個數
,在一個ConcurrentHashMap創建後Segment
的個數是不能變
的,擴容
過程過改變的是每個Segment的大小
。
關於分段鎖
段Segment繼承了重入鎖ReentrantLock
,有了鎖的功能,每個鎖控制的是一段,當每個Segment越來越大時,鎖的粒度
就變得有些大了。
分段鎖的優勢在於保證在操作不同段 map 的時候可以併發執行,操作同段 map 的時候,進行鎖的競爭和等待。這相對於直接對整個map同步synchronized是有優勢的。
缺點在於分成很多段時會比較浪費內存空間(不連續,碎片化); 操作map時競爭同一個分段鎖的概率非常小時,分段鎖反而會造成更新等操作的長時間等待; 當某個段很大時,分段鎖的性能會下降。
jdk1.8的map實現
和hashmap一樣,jdk 1.8中ConcurrentHashmap採用的底層數據結構爲數組+鏈表+紅黑樹的形式。數組可以擴容,鏈表可以轉化爲紅黑樹。
什麼時候擴容?
- 當前容量超過閾值
- 當鏈表中元素個數超過默認設定(8個),當數組的大小還未超過64的時候,此時進行數組的擴容,如果超過則將
鏈表轉化成紅黑樹
什麼時候鏈表轉化爲紅黑樹?
- 當
數組大小已經超過64
並且鏈表中的元素個數超過默認設定(8個)
時,將鏈表轉化爲紅黑樹
ConcurrentHashMap的put操作代碼如下:
/** 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());
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(); // 初始化桶, 數組中每個元素爲一個桶, 桶裏裝的是鏈表或者紅黑樹, 視情況而定
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
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)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
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;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
騰訊雲大佬的上面代碼的講解
把數組中的每個元素看成一個桶。可以看到大部分都是CAS
操作,加鎖的部分是對桶的頭節點進行加鎖
,鎖粒度很小
。
爲什麼不用ReentrantLock而用synchronized ?
減少內存開銷:如果使用ReentrantLock則需要節點繼承AQS
來獲得同步支持,增加內存開銷,而1.8中只有頭節點需要進行同步。
內部優化:synchronized則是JVM直接支持的,JVM能夠在運行時作出相應的優化措施:鎖粗化、鎖消除、鎖自旋等等。