Java中的concurrenthashmap集合詳細剖析
concurrenthashmap
有參構(gòu)造后第一次put時會進行初始化。初始化的容量并不是所傳入的數(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 | 代表此時還未進行初始化操作。 |
sizeCtl為正數(shù) | 若還未初始化則代表的是初始容量,已經(jīng)初始化則代表的是擴容閾值 |
sizeCtl為-1 | 代表正在進行初始化操作 |
sizeCtl為負數(shù)不為-1時 | 代表此時有-1*(1+n)個線程正在進行擴容操作 |
當?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是當存在相同元素時是否覆蓋。這里傳入了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還未進行初始化操作,在第一次寫入數(shù)據(jù)時會進行初始化。 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)//代表還未初始化或者當前長度為0。 2、else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//根據(jù)key的hash值來判斷將要放入哪個位置,并將f指向這個位置。 如果這個位置目前還不存在數(shù)據(jù)的話。就會進入 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; 語句。這里存在一個cas自旋。并且會進行雙重檢測,一旦發(fā)現(xiàn)符合條件,就會將要添加的節(jié)點放入f指向的位置,并跳出循環(huán),結(jié)束。 3、else if ((fh = f.hash) == MOVED)//MOVED為-1,表示當前entry已經(jīng)遷移,里面的tab = helpTransfer(tab, f);表示協(xié)助遷移。遷移完成后或再次循環(huá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時=是負數(shù)。 ok,如果未修改的話他會遍歷桶下面的節(jié)點,判斷是否存在相同的節(jié)點。如果存在的話, 就覆蓋,不存在就加到鏈表尾部。 在添加完成后執(zhí)行: if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } 這部分代碼表示是否進行樹化。如果當前map的長度不小于64,那么當一個桶中的元素大于等于8的時候,會進行 樹化。 最后執(zhí)行addCount(1L, binCount); 這個代碼很復雜??梢詤⒖糷ttps://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的話,會重新進行一次初始化。
在并發(fā)環(huán)境下初始化的時候有可能會有多個線程進入此方法。
不過這里存在一個cas自旋鎖U.compareAndSwapInt(this, SIZECTL, sc, -1),首先,當未初始化的時候sizeCtl為0,所以會進入else if??赡軙卸鄠€線程符合標準進入else if,但是這里存在一個cas自旋鎖,當?shù)谝粋€觸碰到cas的線程進行操作后,sizeCtl就會被修改為-1,如果修改成功則返回true,當另一個線程進入自旋鎖后會發(fā)現(xiàn)sizeCtl, sc并不相等了,所以會返回false,離開else if,這時再吃循環(huán)到if語句時會發(fā)現(xiàn)sizeCtl=-1,然后進入if,執(zhí)行Thread.yield(),謙讓出cpu。
直到初始化完成sizeCtl為擴容閾值為止。此時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集合詳細剖析的文章就介紹到這了,更多相關(guān)concurrenthashmap集合內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring Data JPA中的Specification動態(tài)查詢詳解
Specification是一個設(shè)計模式,用于企業(yè)級應用開發(fā)中,其主要目的是將業(yè)務規(guī)則從業(yè)務邏輯中分離出來,在數(shù)據(jù)查詢方面,Specification可以定義復雜的查詢,使其更易于重用和測試,這篇文章主要介紹了Spring Data JPA中的Specification動態(tài)查詢詳解,需要的朋友可以參考下2023-07-07Spring?Cloud?中使用?Sentinel?實現(xiàn)服務限流的兩種方式
這篇文章主要介紹了Spring?Cloud?中使用?Sentinel?實現(xiàn)服務限流的方式,通過示例代碼主要介紹了Sentinel的兩種實現(xiàn)限流的方式,需要的朋友可以參考下2024-03-03