源碼閱讀 - ConcurrentHashMap#addCount 方法里面的 bug


去年底重擼了部分 ConcurrentHashMap 源碼,當時筆記為 word 形式,比較亂,且剛好當時入職了一家新公司,整理這部分就停下來了(源碼學習這部分在大部分公司里都會沒時間去做,時間全靠擠)。剛好最近讀完部分 redis 內部數據結構實現(雖然 C 語言不是很懂,但應該還是讀懂了重要的部分),正好與Java 這邊的 ConcurrentHashMap 形成對比,CHM 擴容這塊的源碼之后重新整理下就會發上來。

 

為了見證自己曾經擼過 CHM 源碼,並且發現代碼的怪異之處,現在將一些“證據”整理發上來,作為后續 CHM 源碼學習筆記的一個起點......

 

addCount 方法中出 bug 的代碼一如下:

 1         if (check >= 0) {
 2             Node<K,V>[] tab, nt; int n, sc;
 3             while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
 4                    (n = tab.length) < MAXIMUM_CAPACITY) {
 5                 int rs = resizeStamp(n);
 6                 if (sc < 0) {
 7                     if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
 8                         sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
 9                         transferIndex <= 0)
10                         break;
11                     if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
12                         transfer(tab, nt);
13                 }
14                 else if (U.compareAndSwapInt(this, SIZECTL, sc,
15                                              (rs << RESIZE_STAMP_SHIFT) + 2))
16                     transfer(tab, null);
17                 s = sumCount();
18             }
19         }

 

 出 bug 的代碼二定位於 if (sc < 0) 分支下的第一個 if 條件:

1                     if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
2                         sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
3                         transferIndex <= 0)
4                         break;

rs = resizeStamp(n);  這個方法的作用就是對散列表的數組的長度進行標記,其輸出如下:

resize stamp when n=0 : 32800
resize stamp when n=1 : 32799
resize stamp when n=2 : 32798
resize stamp when n=3 : 32798
resize stamp when n=4 : 32797
resize stamp when n=5 : 32797
resize stamp when n=6 : 32797
resize stamp when n=7 : 32797
resize stamp when n=8 : 32796
resize stamp when n=16 : 32795
resize stamp when n=32 : 32794
resize stamp when n=1<<30 : 32769
resize stamp when n=1<<31 : 32768
resize stamp when n=1<<32 : 32799
resize stamp when n=1<<33 : 32798

可以發現 resizeStamp 方法的輸出是固定在 32768~32798 之間的值(n=0和1對應的32799、32800都不會是CHM內部數組的長度值)

32768 的二進制為: 1000 0000 0000 0000,1 個 1 后面跟着 15 個 0,總計 16 位

我們取 n = 4 來分析(相當於創建CHM時指定了 initialCap),n=4 時 rs = 32797 = 32768 + 29, 對應二進制為:1000 0000 0001 1101

 在“代碼一”中有一個 else if 代碼分支,其對應的是第一個進入擴容的線程執行的操作:第一條線程擴容時,首先將原本是正數的 sizeCtl,修改為負數,對應操作:

U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2)
RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS = 16,


CAS操作后 sizeCtl = 1000 0000 0001 1101 0000 0000 0000 0010,對應十進制為:-2145583102(32797<<16=-2145583104),總之肯定是個負數,且 sizeCtl 為負數時其高 16 位保存的是擴容前數組的 sizeStamp。

將出bug的代碼二再貼一遍:

                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)

能進入上述代碼,sc < 0,rs 是正數且取值范圍為 32768~32798,因此不可能出現上述代碼中的“sc == rs + 1” 和 “sc == rs + MAX_RESIZERS”(注:MAX_RESIZERS=65535,是幫助擴容的最大線程數限制)。

而前幾行我們分析過 “sizeCtl 為負數時其高 16 位保存的是擴容前數組的 sizeStamp”,因此這兩處 bug 正確寫法應該是 sc == rs<<16 + 1 和 sc == rs<<16 + MAX_RESIZERS

 

當時比較有意思的是,在參考 https://juejin.im/post/5b001639f265da0b8f62d0f8#comment 這篇 CHM 總結文章分析到 addCount 時發現這個疑慮,搜索 stackoverflow 時有人也提出了同樣的問題,但這位同學更進了一步,直接向 JDK 提出了 BUG,JDK bug link:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8214427。最后這個 BUG 被 JDK 收錄。詳情可以在 bug link 中查看。

這位同學提出 bug 的同時,對 bug 的糾正用的是 <<< ,而我在這篇文章中用的是 <<,寫的時候突然意識到這一點不同,用編譯器(Intellij Idea)驗證了一下,java 中沒有 <<< 這個符號,只有 >>> 。不過這點倒是無傷大雅,整體的思路對了我覺得就達到目的了,一些小細節的地方硬記它有還是沒有作用不大。而且你只需要有一款像我一樣的編譯器,就能提示你 <<< 是錯誤的,這已經能解決問題。

好了,今天這個 bug 的分析到此到一段落,后續會將 CHM 幾個重要方法的分析貼上來,不過我應該還是會繼續用腦圖導出的圖片的方式,可能是一個方法做一張圖,內容的框架和魯道大佬總結的這篇 CHM 應該差不多:https://juejin.im/post/5b001639f265da0b8f62d0f8#comment,沒學習的小伙伴趕緊去看一下吧~

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM