1. 程式人生 > >【源碼閱讀系列】JDK 8 ConcurrentHashMap 源碼分析之 由transfer引發的bug

【源碼閱讀系列】JDK 8 ConcurrentHashMap 源碼分析之 由transfer引發的bug

進一步 輸出 _id 更新 com transfer ase put !=

不閱讀源碼就不會發現這個事兒

前段時間在閱讀ConcurrentHashMap源碼,版本JDK 8,目前源碼研究已經告一段落。感謝魯道的ConcurrentHashMap源碼分析文章,讀到文章,感覺和作者發生了一些交流,解答了很多疑惑,也驗證了一些想法。魯道在簡書的addCount分析文章點這裏 (文章底部的評論中就有這篇文章發酵的原由)。魯道還有其他ConcurrentHashMap源碼分析的系列文章,在簡書、掘金都有分布,感興趣的同學可以進一步追蹤。

推完文章,回到本篇的主題“閱讀源碼”,期間發生了一件有意思的事情,而且既然是個BUG,就提出來讓更多人知道。

ConcurrentHashMap源碼分析導讀

ConcurrentHashMap的源碼據說在 1.8 發生了巨大改變。並發put時,ConcurrentHashMap只會用sync鎖住桶節點(我把table[index] 位置的節點稱為 桶節點),並發度就是hash數組長度。在並發擴容時,每個線程可以一次轉移一個分片區域的桶節點,互不幹擾,詳見transfer源碼 變量 stride ,當然stride最小是16,所以桶不夠的時候,是不會有那麽多線程都在“並發轉移”的。每個線程轉移節點時是從後往前,也就是從下標大的節點往下標小的節點方向來處理轉移,處理完一段分片後,領取下一段,整個舊table處理進度由ConcurrentHashMap#transferIndex屬性控制,它是volatile修飾的,提供更好的可見性。

通過研讀transfer相關的源碼,知道了在 addCount方法中,第一條進去擴容的節點會把 sizeCtl 設為 rs << RESIZE_STAMP_SHIFT) + 2:(MARK-1)

代碼片段:

else if (U.compareAndSwapInt(this, SIZECTL, sc,
                             (rs << RESIZE_STAMP_SHIFT) + 2))

rs的計算方式如下:

int rs = resizeStamp(n);

這個 rs會根據數組長度 n 為 2的多少次冪來進行變化,也就是table長度的一個標識符,取值範圍在 32768~32799 之間。32678是 2^15 等於二進制 1000 0000 0000 0000 。

好了,現在我們已經接近問題現場。根據上面的 MARK-1,第一條線程擴容開始後,sizeCtrl已經是一個負數,而在addCount中你會發現在 MARK-1 的代碼上面還有這麽一段代碼:

if (sc < 0) {
    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
        transferIndex <= 0)
        break;
    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
        transfer(tab, nt);
}

sc < 0 是因為發生擴容,sizeCtl已經為負數,那麽上面這段代碼中 一個負數 sc 如何能與 rs + 1 (rs正數) 和 rs + MAX_RESIZES (結果也是正數)兩個都為正數 進行等值判斷呢???而且 rs + 1 和 rs + MAX_RESIZES 也不是int溢出附近的值。當時是怎麽也想不通負數如何與正數進行比較的,持著懷疑態度我去測試了resizeStamp方法,於是才有前文中我對 rs 取值的驗證。

我的博客後面也會更新自己閱讀ConcurrentHashMap源碼時的一些收獲,盡量把過程和結果輸出出來。

無巧不成書

有意思的是,在上個月底那段時間閱讀源碼碰到這個問題時,開始了各種google,在StackOverFlow上正好有一位同學發表了疑似JDK 8 ConcurrentHashMap的BUG,追蹤進去後,發現oracle已經采納了該BUG。BUG鏈接。而正好就是這位同學,也在魯道的簡書文章下評論了。就是這麽巧。相比提出BUG的這位同學,我的動手能力還有待提高......

終於,最後搞明白了真的就是代碼寫得有問題。該問題還存在於和transfer相關聯的方法,只要是調了 transfer的,如addCount、helpTransfer、tryPresize等方法都有一樣的BUG。

正確寫法本文這裏就不貼出了,相信大家思考一下就能得出結論。BUG清單中也有正解供參考。

總結

閱讀優秀源碼時,敢於質疑,敢於提出猜想,最後用事實去驗證自己的猜想。





【源碼閱讀系列】JDK 8 ConcurrentHashMap 源碼分析之 由transfer引發的bug