能說一下你對ConcurrentHashMap的瞭解嗎?

大家好,這是一個爲了夢想而保持學習的博客。 文章的風格會一直保持問答的方式講述,這是我個人喜歡的一種風格,也是相當於模擬面試。


initTable函數

這個函數就兩個需要注意的點:

1、如果sizeCtl被修改爲負值則讓出CPU,等待下次循環再來看別的線程是否已經創建好了。

2、CAS成功修改sizeCtl之後,需要二次判斷table是否被其他線程創建好了,如果是的話直接返回即可。

    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            // sizeCtl初始值爲0,當小於0的時候表示在別的線程在初始化表或擴展表
            if ((sc = sizeCtl) < 0)
				// 這裏當前線程直接讓出cpu,回到就緒狀態,
                // 等又重新獲取時間片,再次判斷是否初始化完成,
                // 如果其他線程已經完成初始化,下次循環不滿足條件直接退出,tab被賦值後直接返回tab。
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    // 二次判斷,防止併發問題
                    // 最終創建完table後,修改回來sizeCtl爲 cap * 0.75 返回
                    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;
    }

putVal函數

putVal可以分爲三個階段:

1、key/val檢查,如果爲null就拋出NPE。

2、添加元素,一個死循環,直到添加成功纔會break。

3、addCount,統計元素值,判斷是否需要擴容。

	final V putVal(K key, V value, boolean onlyIfAbsent) {
		// 不允許K V爲空
        if (key == null || value == null) throw new NullPointerException();
		// 計算hash值
        int hash = spread(key.hashCode());
        int binCount = 0;
		// 相當於while循環
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
			// 如果未初始化就進行初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
			// 找到對應table上的node節點,並賦值給f,看是否有值。
            // 如果未null直接cas賦值給該table上的節點,不用加鎖
            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);
			// 其餘的情況下,就鎖定f,f代表table上的某一目標節點,鎖定該節點後,
            // 以它爲頭部進入的操作都無法進行了,鎖的粒度變得更小了。
            else {
                V oldVal = null;
                synchronized (f) {
					// 二次判斷(是因爲擴容機制,這裏等待完成後,該源節點會被修改爲fwd節點,
                    // 這裏不滿足條件會直接進行下次循環,去幫助擴容)
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
							// 統計鏈表元素個數
                            binCount = 1;
							// 遍歷鏈表
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
								// 遍歷鏈表的時候,檢查key是否相等
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
									// 如果出現key相等就記錄下oldVal
                                    oldVal = e.val;
									// 如果沒有設置onlyIfAbsent則直接覆蓋舊值,因爲在synchronized不用cas。
                                    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;
							// 將值存入對應的樹結構中,如果有重複的key值則返回p
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
								// 如果沒設置onlyIfAbsent時直接覆蓋
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
				// 如果鏈表個數大於等於8個,則轉換成樹
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
						// 這個函數在table.length<64的時候,會調用tryPresize擴容到最近的2次冪
                        treeifyBin(tab, i);
					//如果有舊值就返回舊值
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
		// 增加一個函數後鎖需要進行的操作(put/remove後都會調用,其中remove傳入addCount(-1L, -1))
		// 1、更新basecount 
        // 2、檢查是否需要transfer擴容,如果第二個參數傳入爲負值,就不會去檢查是否需要擴容
        addCount(1L, binCount);
        return null;
    }
    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }

 


get函數

這個函數跟HashMap的get過程相差不多,簡單說來就是查桶,查樹(查nextTable),查鏈表:

這裏需要注意的一點,整個過程是無鎖且精確的,至於爲什麼能有這樣的效果,我們後面詳細分析。

    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
		// 這裏傳入key爲null則NPR
        int h = spread(key.hashCode());
		// 檢查table,以及對應table對應位置的node是否存在
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
			// 如果就是table上的那個節點直接返回
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
			// 如果是樹節點,查樹後返回。(兩種情況:-1正在擴容、-2樹節點)
			// 如果是forwardingNode節點代表在擴容,則去找對應的實現,去nextTable中查找
            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;
    }

 


transfer函數

transfer的精妙之處,在於可以控制多線程併發協作進行擴容操作,實現這個過程的源碼還是很複雜的,但是描述起來並不難:

從右到左,每個通過CAS設置transferIndex值,來劃分自己擴容的區域,默認是每個線程負責16個桶;

劃分完各自的區域之後,開始進行擴容動作,其實就是鎖定一個桶,然後進行HashMap的resize那一套。

整個過程看起來是這個樣子的:(圖源:https://blog.csdn.net/luzhensmart/article/details/105968886

腦圖:

	  private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
		// 計算步長,每個線程至少處理16個桶
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
		// 最開始初始化這個tab
		if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
            transferIndex = n;
        }
        int nextn = nextTab.length;
		// 創建fwd節點,對應的hash值爲-1。標識該桶已完成。
        // 如果此時get這個桶中的元素就走fwd的重寫函數去nextTab中找,因爲這個節點有nextTab的引用。
		// 如果remove和put就會進來幫助擴容(併發擴容的關鍵)
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
		// 標識一個桶是否已被處理
        boolean advance = true;
        boolean finishing = false; // to ensure sweep before committing nextTab
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            while (advance) {
                int nextIndex, nextBound;
                if (--i >= bound || finishing)
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
				// 從右往左,通過cas修改TRANSFERINDEX的值,這個值代表當前線程處理到哪個桶。(併發擴容關鍵)
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
					// 確定當前線程的操作區間爲[bound, i]
                    // 舉個例子,現在桶內元素有32個,當前線程是第一個進來擴容的線程
                    // 那麼當前線程得到的區間就是 [16, 31]這16個桶
                    // 此時 bound = 32 - stride(16) = 16
                    // i = 32 - 1 = 31
                    // 由於上面修改了TRANSFERINDEX值,
                    // 因此後續進來的線程就會從16開始爲起點,往前劃分自己的擴容區域
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
				// 如果擴容完成,則清空nextTab後修改sizeCtl爲目標值的3/4
                if (finishing) {
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
				// 利用CAS方法更新這個擴容閾值,在這裏面sizectl值減一,說明新加入一個線程參與到擴容操作
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
			// 找到對應table上的節點,賦值給f。如果是null,直接cas就ok。
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
			// 如果已經是fwd節點,則說明已經完成,設置advance = true,讓上面的while循環繼續找下一個節點。
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
			// 其他就是該節點需要被處理的情況
            else {
				// 鎖定當前的table上的元節點,此時如果有put請求,就會被阻塞。
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
						// 如果是鏈表節點
                        if (fh >= 0) {
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            // 拆鏈
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
							// 標誌位爲0的,對應在nextTab的下標不變依舊是i;
                            setTabAt(nextTab, i, ln);
							// 標誌位爲1的,對應在nextTab的下標變爲 n+i;
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
							// 標識該桶已處理完成。
                            advance = true;
                        }
						// 如果是樹節點,和上面的鏈表處理類似
                        else if (f instanceof TreeBin) {
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> lo = null, loTail = null;
                            TreeNode<K,V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node<K,V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode<K,V> p = new TreeNode<K,V>
                                    (h, e.key, e.val, null, null);
                                if ((h & n) == 0) {
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                }
                                else {
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
							// 檢查是否小於閾值,如果是的話,就由紅黑樹轉換爲鏈表
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                (hc != 0) ? new TreeBin<K,V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new TreeBin<K,V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }
        }
    }

 


size & mappingCount函數

size/mappingCount函數都是用於統計當前容器內的元素的,但是size只能返回Integer範圍,mappingCount能返回Long值的範圍,底層都是調用的sumCount函數。

sumCount函數是不精確的,儘管用於計算的所有值都被volatile修飾,但是由於整個累加過程並沒有加鎖,所以對已完成累加的元素,是無法感知到變化的。

舉個例子,在遍歷CounterCell數組時,剛累加了couterCells[0]的值,下一秒對應的元素就被刪了;但是原有的值已經完成累加了,因此在返回時候的值就不再是精確的了。

但是,在如此高併發的情況下,size的也沒有什麼參考價值,能有個大概就行,因此沒必要因爲這種函數而加全局的鎖。

	// 核心統計函數,裏面涉及的變量
	// baseCount:被volatile修飾,在增刪元素,並且沒有競爭的時候會直接通過cas修改它(addCount函數)。
	// counterCells:如果併發失敗,就初始化這個數組,再用cas寫入CounterCell對象。
	// CounterCell:就是上面那個數組的內部元素對象,被@sun.misc.Contended修飾,
    // 內部val被volatile修飾,val就是被賦值加了幾個。
	final long sumCount() {
		// 當 counterCells 不是 null,就遍歷元素,並和 baseCount 累加。
        CounterCell[] as = counterCells; CounterCell a;
        long sum = baseCount;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

 


常見問題

1)是如何實現線程安全控制的?

syn+volatile+cas控制的,比如在put的時候,如果元節點爲null則直接cas賦值,如果已有值,就通過syn鎖定操作。而volatile保證了內存可見性。

 

2)get函數爲什麼不用加鎖?

首先,table變量是被volatile修飾的,而volatile的內存語義表示內存可見,也就是A線程對volatile的修改一定對下一個線程B的讀可見。
那麼在擴容的時候,如果修改了table引用,當前線程是能準確找到擴容完成的table對象的。
其次,我們table數組中的元素,在獲取的時候,是通過unsafe.getObjectVolatile獲取的,以保證數組中的元素變動是內存可見的。
最後,我們的Node節點,內部的val和next都是被volatile修飾的,因此我們在獲取當前Node值,以及通過next指針遍歷鏈表/樹/nextTable的時候拿到的也都是精確的值。
而對應的修改操作,例如PUT/REMOVE等,都是putObjectVolatile或者sync加鎖的,以保證只有一個線程可以修改node,保證了內存可見。
所以,綜上所述,get操作哪怕是不加鎖,拿到的也是精確的值。

 

3)對應擴容的描述,是何如做到多線程合作去擴容的?

主要是通過ForwardingNode和TRANSFERINDEX。
TRANSFERINDEX記錄了當前線程的操作下標,多個線程併發擴容的時候都會根據這個值從右到左去劃分一個自己的擴容區域,
然後依次去擴容,最後擴容完成後,就設置table中的元節點爲ForwardingNode,以此來標識當前桶擴容完成。

 

4)擴容的時候,get/put/remove操作是如何進行的呢?

如果get找到的節點是還沒操作過的節點,直接遍歷返回,如果是fwd節點則是已完成轉移的節點,則通過fwd節點的nextTab引用去查對應的桶。
put操作,如果是還沒處理到的節點,就會正常鎖定去添加元素,最後執行addCount,去幫助擴容。
                如果是正在處理的節點,因爲拿不到該元節點的值而需要等待鎖。
                如果是已完成的節點,就直接去幫助擴容。
remove操作,和put一樣,還沒處理到就正常刪除,執行addCount,但是因爲傳入的是-1,是不會去幫助擴容的,修改了basecount後就直接返回了。
如果正在處理就需要等待,拿到鎖後再次判斷節點屬性,成爲fwd後去幫助擴容,如果是已完成的節點,就去幫助擴容。

 

5)針對於1.7做了哪些優化?爲什麼捨棄了分段鎖。

首先捨棄了分段鎖,而選用了syn去鎖住table中的元數據節點,這減小了鎖的粒度。
其次就是放棄了reentrantLock,是因爲syn做了很多優化,比之前的syn性能要好了很多。
最後就是到達8個元素的時候,轉換成紅黑樹提高查詢性能。鏈表:好查(其實也是遍歷,也慢),查慢。紅黑樹:好查,不好插,因爲要自旋。還有更高效的擴容。
總結:1、捨棄分段鎖,使用syn鎖住元節點,減小鎖的粒度,另外就是syn做了很多優化。
           2、鏈表長度達到8時轉換成紅黑樹提高查詢效率。
           3、多線程協作擴容,提升效率。

 

6)什麼時候會觸發擴容?

1、如果是table小於64而又有某個桶的元素個數大於8個這個時候會觸發。
2、新增節點後,如果當前元素個數大於閾值則進行擴容。

 

7)size函數爲什麼是弱一致性的?

可以拿到瞬時的準確值,但是在累加過程中沒有加鎖,所以還是弱一致性。
併發一旦高了,這一時刻的值其實並沒有太大參考意義。

 

8)鏈表向紅黑樹的轉換?

首先在putVal函數裏面,判斷一個桶中的元素大於閾值8的時候,就會執行treeifyBin函數。
這個函數先去檢查table.len是否小於64,如果小直接走擴容。
如果大,那就遍歷鏈表生成新的鏈表,鏈表元素爲TreeNode。然後調用TreeBin的構造器,去構造紅黑樹。
得到的TreeBin節點是一個空節點,hash值爲-2,放在table中,其中保存root的指針。
構造紅黑樹的過程中,每添加完一個節點,都會調用balanceInsertion函數去維持紅黑樹的屬性和平衡性。紅黑樹的所有操作時間複雜度都是O(logN)。
查找的時間複雜度是2LogN,但是前面的常量可以不計。


 

最後重要屬性概述

ForwardingNode:標識擴容完成,空節點,但是保存nextTab的引用,hash值爲-1.
TreeBin:標識該桶爲紅黑樹,空節點,但是保存root的引用,hash爲-2.
baseCount:增刪時用來對元素個數的統計,只用於size和mappingCount函數。
CounterCell:val被volatile修飾,在元素增刪的時候發生競爭就用這個記錄,這個類做了防止僞共享的操作。
CounterCells:上面那個對象的數組,也被volatile修飾,也是主要用於size和mappingCount函數。 
sizeCtl:閾值,一般是len的3/4大小。一旦出現元素個數大於它,就會觸發擴容。
nextTable:擴容的時候初始化,主要用於擴容,其大小是元素組的兩倍,是高效率擴容的關鍵。
transferIndex:多線程併發擴容的關鍵,主要是記錄從右到左,擴容的操作到了哪個節點了。
table:Node數組,concurrenthashmap的本體。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章