1、它實現了ConcurrentMap
接口,該接口定義了一些原子操作約定
2、線程安全
- 完全的并發讀和高并發寫
- 讀操作完全無鎖,犧牲了一致性;寫操作部分有鎖
- 它與
HashTable
、Collections.synchronizedMap
-
HashMap
支持null
,ConcurrentHashMap
、HashTable
不支持null
3、java7
- 分段鎖
- 哈希表/鏈表
4、java8
-
CAS
+Unsafe
- 哈希表/鏈表 + 紅黑樹
java7
的實現
一、相關概念
1、分段鎖
ConcurrentHashMap
底層采用多個分段Segment
,每段下面都是一個哈希表,這就是分段。每當需要對每段數據上鎖操作時,只需要對Segment
上鎖即可,這就是分段鎖。通常稱Segment
的數量叫做并發度concurrency
。
優點:
-
在未上鎖的情況下,提高了并發度;
file
2、并發度concurrency
/**
* The default concurrency level for this table, used when not
* otherwise specified in a constructor.
*/
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
這表示默認情況下,會有16個段
Segment
3、每個Segment
的哈希表長度都是2的冪次方
在ConcurrentHashMap
構造方法中
二、源碼分析
1、get
方法
- 計算
segment
的位置 - 找到這個段下面的哈希表
- 遍歷鏈表,看是否存在
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
// 獲取到key所在Segment數組的下標
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
// 判斷這個下標是否存在,以及Segment下面的哈希表是否存在
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
// 熟悉的:(tab.length - 1) & h操作
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
(1)為什么要使用UNSAFE.getObjectVolatile(segments, u)
這種方式來讀取數組下標的某個元素?
提高性能。使用常用segments[i]
這種語法,在編譯字節碼的時候,是會檢查數組是否越界;而使用上面的代碼,會節省這一步。
(2)如何保證線程安全性?
即如何保證在多線程環境下,當線程在做更新操作時,如果其他線程在同步讀的話,是可能出現臟數據、空指針情況。那么ConcurrentHashMap
是如何保證的?
ConcurrentHashMap
為了提高高并發,而犧牲了一致性,但這種一致性是弱一致性,不會對程序造成大的過錯。所以臟數據是無法避免的,因此在java8
的類注釋寫到不建議使用size
、isEmpty
、containsValue
來進行判斷語句。
* Bear in mind that the results of aggregate status methods including
* {@code size}, {@code isEmpty}, and {@code containsValue} are typically
* useful only when a map is not undergoing concurrent updates in other threads.
* Otherwise the results of these methods reflect transient states
* that may be adequate for monitoring or estimation purposes, but not
* for program control.
2、put
方法
- 找到
Segment
,必要時新建; -
Segment
執行put
操作,必要時擴容;
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
(1)擴容時如何保證線程安全性?
- 在創建
Segment
時,采用CAS
保證線程安全性; - 在創建
Entry
時,因為Segment
本身就是ReentrantLock
,在其Segment.put()
方法是一定保證在獲取到鎖的情況下才執行操作的;
(2)Unsafe.getObject()
的作用?
java8
的實現
一、與java7
的改進
使用哈希表 + 鏈表/紅黑樹 的數據結構
file
放棄使用分段鎖,改用CAS
、volatile
、Unsafe
java7
的分段鎖很好,但鎖畢竟還是很慢的,所以java8
實現了盡可能地無鎖環境。
這里所說地無鎖也僅僅大多數情況下,在某些特殊場景還是需要鎖地。
鎖的粒度更細
java7
鎖地粒度是Segment
,而在java8
中鎖地粒度是每個Entry
二、源碼分析
1、get
方法
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 重新hash
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;
}
// 如果元素地hash值小于0,就往紅黑樹查找
else if (eh < 0)
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;
}
(1)查找沒有鎖,如何有人在寫入怎么辦?
- 在紅黑樹狀態下,查找是有讀寫鎖;
- 在鏈表狀態下,跟
java7
相似,犧牲了弱一致性;
2、put
方法
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
// 重新hash
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();
// 找到對應下標Entry,如果為空,就新建
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);
// 在對應Entry下,進行put操作
else {
V oldVal = null;
// synchronized鎖定entry,進行put
synchronized (f) {
if (tabAt(tab, i) == f) {
// 鏈表地put操作
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;
}
}
}
// 紅黑樹地put操作
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;
}