先看問題:
- ConcurrentHashMap是怎么做到線程安全的?
- get方法如何線程安全地獲取key、value?
- put方法如何線程安全地設(shè)置key、value?
- size方法如果線程安全地獲取容器容量?
- 底層數(shù)據(jù)結(jié)構(gòu)擴(kuò)容時(shí)如果保證線程安全?
- 初始化數(shù)據(jù)結(jié)構(gòu)時(shí)如果保證線程安全?
volatile變量(sizeCtl):它是一個(gè)標(biāo)記位,用來告訴其他線程這個(gè)坑位 有沒有人在,其線程間的可見性由volatile保證。
CAS操作:CAS操作保證了設(shè)置sizeCtl標(biāo)記位的原子性,保證了只有一個(gè)線程能設(shè)置成功- ConcurrentHashMap并發(fā)效率是如何提高的?
- 和加鎖相比較,為什么它比HashTable效率高?
//node的定義
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
// volatile 關(guān)鍵字修飾
volatile V val;
volatile Node<K,V> next;
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
//key 或 value都不能為空
//這是因?yàn)楫?dāng)通過 get(k) 獲取對應(yīng)的 value 時(shí),如果獲取到的是 null 時(shí),無法判斷,它是 put(k,v) 的時(shí)候 value 為 null,還是這個(gè) key 從來沒有添加。
//**1.這個(gè)key從來沒有在map中映射過。
//**2.這個(gè)key的value在設(shè)置的時(shí)候,就是null。
//在非線程安全的map集合(HashMap)中可以使用map.contains(key)方法來判斷,而ConcurrentHashMap卻不可以。hashmap正確使用場景是單線程下,由于是單線程,當(dāng)?shù)玫降膙alue是null的時(shí)候,可以用hashMap.containsKey(key)方法來區(qū)分上面說的兩重含義(保證在單線程下 線程安全 不存在并發(fā)場景,所以不會(huì)存在二義性)。
if (key == null || value == null) throw new NullPointerException();
//獲取hashcode
int hash = spread(key.hashCode());
int binCount = 0;
//遍歷數(shù)組中的node
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//Node數(shù)組為空,初始化Node數(shù)組
if (tab == null || (n = tab.length) == 0)
//看下面有詳細(xì)解釋
tab = initTable();
//該node節(jié)點(diǎn)中沒有元素,CAS對該位置的節(jié)點(diǎn)進(jìn)行原子操作
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
//插入成功,退出循環(huán)
break; // no lock when adding to empty bin
}
//如果Node的hash值等于-1,map進(jìn)行擴(kuò)容
else if ((fh = f.hash) == MOVED)
//幫助擴(kuò)容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//鎖的是數(shù)組槽里面的node
synchronized (f) {
//二次確認(rèn)此Node對象還是原來的那一個(gè)
//其使用Unsafe類volatile的操作volatile式地查看值,保證每次獲取到的值都是最新的:
if (tabAt(tab, i) == f) {
//為什么大于等于0是代表的鏈表,小于0代表是紅黑樹?
//定義的常量 static final int TREEBIN = -2; // hash for roots of trees
//構(gòu)造函數(shù)里面調(diào)用super,會(huì)將-2作為hash
//感覺沒必要,直接像hashMap一樣先判斷是否紅黑樹,然后else里面處理鏈表邏輯
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
//這個(gè)if是判斷在鏈表上是否存在key相同,若相同,則根據(jù)onlyIfAbsent的結(jié)果考慮是否進(jìn)行替換
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
//感覺沒必要申明這個(gè)對象,下面直接用e.next.next就可以了
Node<K,V> pred = e;
//尾插法,插入節(jié)點(diǎn)
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;
}
// 初始化數(shù)組
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
//sizeCtl是一個(gè)標(biāo)記位,若為-1也就是小于0,代表有線程在進(jìn)行初始化工作了
if ((sc = sizeCtl) < 0)
//讓出CPU時(shí)間片
Thread.yield(); // lost initialization race; just spin
//CAS操作,將本實(shí)例的sizeCtl變量設(shè)置為-1,類似上個(gè)樂觀鎖,讓別人知道我在開始初始化數(shù)組了
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
//再檢查一遍數(shù)組是否為空
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
//Node數(shù)組
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
//通過位運(yùn)算,n減去n二進(jìn)制右移2位,相當(dāng)于乘以0.75
//例如16經(jīng)過運(yùn)算為12,與乘0.75一樣,只不過位運(yùn)算更快
sc = n - (n >>> 2);
}
} finally {
//將計(jì)算后的sc(12)直接賦值給sizeCtl,表示達(dá)到12長度就擴(kuò)容
//由于這里只會(huì)有一個(gè)線程在執(zhí)行,直接賦值即可,沒有線程安全問題
//只需要保證可見性
sizeCtl = sc;
}
break;
}
}
return tab;
}