前言
我們之前分析了Hash的源碼,主要是 put 方法宦棺。同時瓣距,我們知道,HashMap 在并發(fā)的時候是不安全的代咸,為什么呢蹈丸?因為當多個線程對 Map 進行擴容會導致鏈表成環(huán)。不單單是這個問題呐芥,當多個線程相同一個槽中插入數(shù)據(jù)逻杖,也是不安全的。而在這之后思瘟,我們學習了并發(fā)編程荸百,而并發(fā)編程中有一個重要的東西,就是JDK 自帶的并發(fā)容器滨攻,提供了線程安全的特性且比同步容器性能好出很多够话。一個典型的代表就是 ConcurrentHashMap,對光绕,又是 HashMap 女嘲,但是這個 Map 是線程安全的,那么同樣的诞帐,我們今天就看看該類的 put 方法是如何實現(xiàn)線程安全的欣尼。
源碼加注釋分析 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());
int binCount = 0;
// 死循環(huán)執(zhí)行
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
// 初始化
tab = initTable();
// 獲取對應下標節(jié)點,如果是kong停蕉,直接插入
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// CAS 進行插入
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 如果 hash 沖突了愕鼓,且 hash 值為 -1钙态,說明是 ForwardingNode 對象(這是一個占位符對象,保存了擴容后的容器)
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 如果 hash 沖突了拒啰,且 hash 值不為 -1
else {
V oldVal = null;
// 同步 f 節(jié)點驯绎,防止增加鏈表的時候?qū)е骆湵沓森h(huán)
synchronized (f) {
// 如果對應的下標位置 的節(jié)點沒有改變
if (tabAt(tab, i) == f) {
// 并且 f 節(jié)點的hash 值 不是大于0
if (fh >= 0) {
// 鏈表初始長度
binCount = 1;
// 死循環(huán),直到將值添加到鏈表尾部谋旦,并計算鏈表的長度
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;
}
}
}
// 如果 f 節(jié)點的 hasj 小于0 并且f 是 樹類型
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;
}
}
}
}
// 鏈表長度大于等于8時剩失,將該節(jié)點改成紅黑樹樹
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 判斷是否需要擴容
addCount(1L, binCount);
return null;
}
樓主在代碼中寫了很多注釋,但是還是說一下步驟(該方法和HashMap 的高度相似册着,但是多了很多同步操作)拴孤。
- 校驗key value 值,都不能是null甲捏。這點和 HashMap 不同演熟。
- 得到 key 的 hash 值。
- 死循環(huán)并更新 tab 變量的值司顿。
- 如果容器沒有初始化芒粹,則初始化。調(diào)用 initTable 方法大溜。該方法通過一個變量 + CAS 來控制并發(fā)化漆。稍后我們分析源碼。
- 根據(jù) hash 值找到數(shù)組下標钦奋,如果對應的位置為空座云,就創(chuàng)建一個 Node 對象用CAS方式添加到容器。并跳出循環(huán)付材。
- 如果 hash 沖突朦拖,也就是對應的位置不為 null,則判斷該槽是否被擴容了(-1 表示被擴容了)厌衔,如果被擴容了璧帝,返回新的數(shù)組。
- 如果 hash 沖突 且 hash 值不是 -1富寿,表示沒有被擴容睬隶。則進行鏈表操作或者紅黑樹操作,注意作喘,這里的 f 頭節(jié)點被鎖住了理疙,保證了同時只有一個線程修改鏈表晕城。防止出現(xiàn)鏈表成環(huán)泞坦。
- 和 HashMap 一樣,如果鏈表樹超過8砖顷,則修改鏈表為紅黑樹贰锁。
- 將數(shù)組加1(CAS方式)赃梧,如果需要擴容,則調(diào)用 transfer 方法(非常復雜豌熄,以后再詳解)進行移動和重新散列授嘀,該方法中,如果是槽中只有單個節(jié)點锣险,則使用CAS直接插入蹄皱,如果不是,則使用 synchronized 進行同步芯肤,防止并發(fā)成環(huán)粪小。
這里說一說 initTable 方法:
/**
* Initializes table, using the size recorded in sizeCtl.
*/
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
// 小于0說明被其他線程改了
if ((sc = sizeCtl) < 0)
// 自旋等待
Thread.yield(); // lost initialization race; just spin
// CAS 修改 sizeCtl 的值為-1
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
// sc 在初始化的時候用戶可能會自定義静檬,如果沒有自定義,則是默認的
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
// 創(chuàng)建數(shù)組
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
// sizeCtl 計算后作為擴容的閥值
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
該方法為了在并發(fā)環(huán)境下的安全,加入了一個 sizeCtl 變量來進行判斷绳瘟,只有當一個線程通過CAS修改該變量成功后(默認為0,改成 -1)毡琉,該線程才能初始化數(shù)組蕾久。保證了初始化數(shù)組時的安全性。
總結(jié)
ConcurrentHashMap 是并發(fā)大師 Doug Lea 的杰作歌豺,可以說鬼斧神工推穷,總的來說,使用了 CAS 加 synchronized 來保證了 put 操作并發(fā)時的危險(特別是鏈表)世曾,相比 同步容器 hashTable 來說缨恒,如果容器大小是16,并發(fā)的性能是他的16倍轮听,注意骗露,讀的時候是沒有鎖的,完全并發(fā)血巍,而 HashTable 在 get 方法上直接加上了 synchronized 關(guān)鍵字萧锉,性能差距不言而喻。
當然述寡,樓主這篇文章可能之寫到了 ConcurrentHashMap 的皮毛柿隙,關(guān)于如何擴容,樓主沒有詳細介紹鲫凶,而樓主在閱讀源碼的收獲也很多禀崖,發(fā)現(xiàn)了很多有趣的東西,比如 ThreadLocalRandom 類在 addCount 方法中的應用螟炫,大家可以看看該類波附,非常的實用。
注意:這篇文章僅僅是 ConcurrentHashMap 的開頭,關(guān)于 ConcurrentHashMap 里面的精華太多掸屡,值得我們好好學習封寞。
good luck !=霾啤1肪俊!盏求!