今天咱們就不大戰禿頭老了,給自己充充電,日后再戰禿頭老!
注:ConcurrentHashMap聊的是1.8之后的!
兄弟姐妹們都說HashMap執行緒不安全,想執行緒安全就用ConcurrentHashMap,那為什么它就執行緒安全那?啊?為什么呢!不用想,肯定從put()方法看起來嘛!那有請put()老哥,出來溜達一下唄!
// 哎呦,老面孔了,跟HashMap一樣都呼叫了putVal方法
public V put(K key, V value) {
return putVal(key, value, false);
}
// 哎呦,final修飾的方法,不允許被重寫,看來很自信呀!
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 傳遞值非空校驗
if (key == null || value == null) throw new NullPointerException();
// 通過 key 的 hashcode 求出 hash 值,即元素所在位置
int hash = spread(key.hashCode());
int binCount = 0;
// 遍歷table
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 判斷是否需要初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 如果當前要put的位置為空,則通過 cas+自旋 方式放入,不加鎖,成功后跳出回圈
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;
}
綜上所述,就是加鎖了!簡單的描述下:
1:通過hashcode求出key應該所放的位置
2:是否已經初始化,如果沒有則呼叫initTable()
3:當前位置的值是否為空,為空的則通過CAS樂觀鎖的方式添加元素(兄弟,別慌,后面會說CAS)
4:判斷是否需要擴容
5:加synchronized來添加元素,如果是鏈表則遍歷鏈表添加元素,如果是紅黑樹則呼叫紅黑樹添加元素的方法!
既然我們知道了為啥執行緒安全了,那我們順帶看一下initTable()方法唄,看不了上當,看不了吃虧!
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
// 是否需要初始化
while ((tab = table) == null || tab.length == 0) {
// sizeCtl 這個玩意老牛了
// 如果 = -1 說明有其他執行緒在初始化,
// -N 說明N-1個執行緒在擴容
// >0 代表容量
if ((sc = sizeCtl) < 0)
// 既然,有其他執行緒在初始化,那咱們就禮讓一下唄!
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {// 通過cas方式進行判斷是否要初始化!(又是CAS,好奇不!)
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
// 把容量大小賦值給sizeCtl,是不是說明 >0 代表容量
sizeCtl = sc;
}
break;
}
}
return tab;
}
簡單說就是,如果需要初始化,首先看看有沒有其他執行緒在初始化,有就禮讓,沒有就讓我來!特別注意sizeCtl!
看到兩次CAS了,是不得說說,誒!先不說,先看看get(),這個函式很常用,順帶看下,按住躁動的心,get()非常簡單!
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 求key所在的文職
int h = spread(key.hashCode());
// 是否已經初始化過了
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 指定元素的hash如果和頭節點的hash相同,就直接回傳頭節點的值
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:通過key的hashcode求key的位置
2:判斷是不是頭節點,是就直接回傳
3:如果頭節點的hash小于0,不是在擴容,就是紅黑樹,通過find查找
4:最后就是鏈表了,遍歷查找!
終于說完了ConcurrentHashMap,那是不是逼逼一下CAS那,害,咱們就放到下次充電唄!
嗯…想到看官老爺們,連個贊都不留下, 只有默默的閱讀量,哎,看官老爺來個贊把!,下次咱們說說CAS,保證通俗易懂哦!(主要是我還沒掌握透徹,等掌握透徹再來哦!)
參考文章:Guide哥的Java學習
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/397568.html
標籤:其他
