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

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

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

前段時間在閱讀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清單中也有正解供參考。

 

總結

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