Java中的concurrenthashmap集合詳細(xì)剖析
concurrenthashmap
有參構(gòu)造后第一次put時會進(jìn)行初始化。初始化的容量并不是所傳入的數(shù)。
源碼:
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
由源碼可知,會先判斷所傳入的容量是否>=最大容量的一半,如果滿足條件,就會將容量修改為最大值,反之則會將容量改為所傳入數(shù)*1.5+1。
并且這個tableSizeFor函數(shù)會將這個數(shù)修改為2**n>initialCapacity + (initialCapacity >>> 1) + 1的最小2**n。
舉個例子證明一下:
ConcurrentHashMap<String,String> a = new ConcurrentHashMap<>(32);
Class<? extends ConcurrentHashMap> aClass = a.getClass();
Field sizeCtl = aClass.getDeclaredField("sizeCtl");
sizeCtl.setAccessible(true);
System.out.println(sizeCtl.getName()+":"+sizeCtl.get(a));
輸出如下:
sizeCtl:64
這里的sizeCtl有四種情況。
| 類型 | 含義 |
| sizeCtl為0 | 代表此時還未進(jìn)行初始化操作。 |
| sizeCtl為正數(shù) | 若還未初始化則代表的是初始容量,已經(jīng)初始化則代表的是擴(kuò)容閾值 |
| sizeCtl為-1 | 代表正在進(jìn)行初始化操作 |
| sizeCtl為負(fù)數(shù)不為-1時 | 代表此時有-1*(1+n)個線程正在進(jìn)行擴(kuò)容操作 |
當(dāng)?shù)谝淮螌懭霐?shù)據(jù)時:
源碼如下:
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) {//onlyIfAbsent是當(dāng)存在相同元素時是否覆蓋。這里傳入了false就是覆蓋的意思。
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());//計算key對象的hash值
int binCount = 0;
for (Node<K,V>[] tab = table;;) {//這里是一個死循環(huán)。
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)//如果table還未進(jìn)行初始化操作,在第一次寫入數(shù)據(jù)時會進(jìn)行初始化。
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;
}
for (Node<K,V>[] tab = table;;) {//這里是一個死循環(huán)。這個循環(huán)中存在4個判斷
分別是:
1、if (tab == null || (n = tab.length) == 0)//代表還未初始化或者當(dāng)前長度為0。
2、else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//根據(jù)key的hash值來判斷將要放入哪個位置,并將f指向這個位置。
如果這個位置目前還不存在數(shù)據(jù)的話。就會進(jìn)入
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; 語句。這里存在一個cas自旋。并且會進(jìn)行雙重檢測,一旦發(fā)現(xiàn)符合條件,就會將要添加的節(jié)點放入f指向的位置,并跳出循環(huán),結(jié)束。
3、else if ((fh = f.hash) == MOVED)//MOVED為-1,表示當(dāng)前entry已經(jīng)遷移,里面的tab = helpTransfer(tab, f);表示協(xié)助遷移。遷移完成后或再次循環(huán)并進(jìn)行判斷。
4、else中有一把瑣synchronized (f) 。在上面的代碼中i已經(jīng)被賦值為(n - 1) & hash)。
這把鎖所包括的代碼塊中
存在雙重檢測代碼if (tabAt(tab, i) == f)。
他的存在就是為了檢測曾經(jīng)獲得的桶的頭節(jié)點的位置是否已經(jīng)修改。
因為在map中存在樹-->鏈表和鏈表->樹的操作,這回改變桶頭節(jié)點的位置。
但是這并不能保證安全,
因為在轉(zhuǎn)化后頭節(jié)點的位置可能不會發(fā)生改變,所以還存在if (fh >= 0)這一句代碼,
表示f這個節(jié)點的hash。
樹節(jié)點的hash時=是負(fù)數(shù)。
ok,如果未修改的話他會遍歷桶下面的節(jié)點,判斷是否存在相同的節(jié)點。如果存在的話,
就覆蓋,不存在就加到鏈表尾部。
在添加完成后執(zhí)行:
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
這部分代碼表示是否進(jìn)行樹化。如果當(dāng)前map的長度不小于64,那么當(dāng)一個桶中的元素大于等于8的時候,會進(jìn)行
樹化。
最后執(zhí)行addCount(1L, binCount);
這個代碼很復(fù)雜。可以參考https://www.bilibili.com/video/BV17i4y1x71z?from=search&seid=2906009396999368521&spm_id_from=333.337.0.0
初始化源碼:
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
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 = sc;
}
break;
}
}
return tab;
}
我的理解是在嗎map初始化之后,map的長度縮減到0的話,會重新進(jìn)行一次初始化。
在并發(fā)環(huán)境下初始化的時候有可能會有多個線程進(jìn)入此方法。
不過這里存在一個cas自旋鎖U.compareAndSwapInt(this, SIZECTL, sc, -1),首先,當(dāng)未初始化的時候sizeCtl為0,所以會進(jìn)入else if。可能會有多個線程符合標(biāo)準(zhǔn)進(jìn)入else if,但是這里存在一個cas自旋鎖,當(dāng)?shù)谝粋€觸碰到cas的線程進(jìn)行操作后,sizeCtl就會被修改為-1,如果修改成功則返回true,當(dāng)另一個線程進(jìn)入自旋鎖后會發(fā)現(xiàn)sizeCtl, sc并不相等了,所以會返回false,離開else if,這時再吃循環(huán)到if語句時會發(fā)現(xiàn)sizeCtl=-1,然后進(jìn)入if,執(zhí)行Thread.yield(),謙讓出cpu。
直到初始化完成sizeCtl為擴(kuò)容閾值為止。此時sizeCtl已經(jīng)不滿足if語句了,所以那些還在謙讓的線程就會開始陸陸續(xù)續(xù)的執(zhí)行else if語句,發(fā)現(xiàn)cas返回true,然后再else if中存在雙重檢測,判斷table是否為null。很顯然并不滿足。
于是執(zhí)行finally中的語句,最后跳出循環(huán),返回table。
到此這篇關(guān)于Java中的concurrenthashmap集合詳細(xì)剖析的文章就介紹到這了,更多相關(guān)concurrenthashmap集合內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring Data JPA中的Specification動態(tài)查詢詳解
Specification是一個設(shè)計模式,用于企業(yè)級應(yīng)用開發(fā)中,其主要目的是將業(yè)務(wù)規(guī)則從業(yè)務(wù)邏輯中分離出來,在數(shù)據(jù)查詢方面,Specification可以定義復(fù)雜的查詢,使其更易于重用和測試,這篇文章主要介紹了Spring Data JPA中的Specification動態(tài)查詢詳解,需要的朋友可以參考下2023-07-07
Spring?Cloud?中使用?Sentinel?實現(xiàn)服務(wù)限流的兩種方式
這篇文章主要介紹了Spring?Cloud?中使用?Sentinel?實現(xiàn)服務(wù)限流的方式,通過示例代碼主要介紹了Sentinel的兩種實現(xiàn)限流的方式,需要的朋友可以參考下2024-03-03
詳解SpringBoot可執(zhí)行Jar包運(yùn)行原理
SpringBoot有一個很方便的功能就是可以將應(yīng)用打成可執(zhí)行的Jar,那么大家有沒想過這個Jar是怎么運(yùn)行起來的呢,本篇博客就來介紹下 SpringBoot可執(zhí)行Jar包的運(yùn)行原理,需要的朋友可以參考下2023-05-05

