去年底重擼了部分 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,沒學習的小伙伴趕緊去看一下吧~