剛剛在頭條看見一個說CHM(ConcurrentHashMap)在jdk8中的bug,自己親自試了一下確實存在,並按照頭條帖里面說的看了一下源碼,記錄一下
CHM的computeIfAbsent的方法是jdk8中新加的方法,也應用了jdk8的新特性,函數接口,lambda表達式;
方法說明:
public V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction)
如果指定的鍵尚未與值相關聯,則嘗試使用給定的映射函數計算其值,並將其輸入到此映射中,除非
null
。 整個方法調用是以原子方式執行的,因此每個鍵最多應用一次該函數。 在計算過程中可能會阻止其他線程對該映射進行的某些嘗試更新操作,因此計算應該簡單而簡單,不得嘗試更新此映射的任何其他映射。
- Specified by:
-
computeIfAbsent
在界面ConcurrentMap<K,V>
- Specified by:
-
computeIfAbsent
在界面Map<K,V>
- 參數
-
key
- 指定值與之關聯的鍵 -
mappingFunction
- 計算值的函數 - 結果
- 與指定鍵相關聯的當前(現有或計算)值,如果計算值為空,則為null
bug說明:
說明,jdk8:conmputeIfAbsent方法,獲取key1的value,當值不存在是用mappingFunction返回的值設置到key1的value中
如果mappingFunction的返回邏輯也是用當前map的computeIfAbsent方法返回另外一個key2的值,
而恰巧這兩個key,key1和key2的hashCode值一樣,即對應同一個table的槽,還有死循環的可能
正常的用法:
/** * 測試concurrentHashMapd的computIfAbsent方法bug * 說明,jdk8:conmputeIfAbsent方法,獲取key1的value,當值不存在是用mappingFunction返回的值設置到key1的value中 * 如果mappingFunction的返回邏輯也是用當前map的computeIfAbsent方法返回另外一個key2的值,而恰巧這兩個key,key1和key2的hashCode值一樣,即對應同一個table的槽,還有死循環的可能 */ public class CopmuteIfAbsentTest { public static void main(String[] args) { System.out.println("AaAa".hashCode()); System.out.println("BBBB".hashCode()); //正常用法 Map<String,String> map2 = new ConcurrentHashMap<>(); String value1 = map2.computeIfAbsent("AaAa",n->"123"); System.out.println(value1); //bug重現 /*map2.computeIfAbsent("AaAa",(n)->{ return map2.computeIfAbsent("BBBB",m->"123"); });*/ } }
輸出結果:
2031744
2031744
123
Process finished with exit code 0
將上面代碼正常用法注釋掉,放開bug重現部分,執行結果如果下:
2031744
2031744
代碼一直沒有退出,死循環
方法源 1 public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) 2 if (key == null || mappingFunction == null)
3 throw new NullPointerException(); 4 int h = spread(key.hashCode()); //"AaAa"和"BBBB"的hash值相同,定位到tab的相同位置 5 V val = null; 6 int binCount = 0; 7 for (Node<K,V>[] tab = table;;) { //此處死循環出不來 8 Node<K,V> f; int n, i, fh; 9 if (tab == null || (n = tab.length) == 0) //①測試代碼第一次,ComputeIfAbsent("AaAa",...) 進入死循環后第一次走這tab初始化,第二個computeIfAbsent("BBBB",)不走這 10 tab = initTable(); 11 else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {② //AaAa的computeIfAbsent方法發現此處table為空,創建占位Node,放進去,第二個computeIfAbsent("BBBB",)不走這 12 Node<K,V> r = new ReservationNode<K,V>(); 13 synchronized (r) { 14 if (casTabAt(tab, i, null, r)) { 15 binCount = 1; 16 Node<K,V> node = null; 17 try { 18 if ((val = mappingFunction.apply(key)) != null) //③在此處調用lambda表達式,進行計算等待返回值 19 node = new Node<K,V>(h, key, val, null); 20 } finally { 21 setTabAt(tab, i, node); //將計算結果創建node,頂替掉占位node 22 } 23 } 24 } 25 if (binCount != 0) 26 break; 27 } 28 else if ((fh = f.hash) == MOVED) //③判斷是否正在擴容,第二次computeIfAbsent時,f為占位node,hash為-3,不滿足 29 tab = helpTransfer(tab, f); 30 else { //④ 31 boolean added = false; 32 synchronized (f) { 33 if (tabAt(tab, i) == f) { 34 if (fh >= 0) { //⑤判斷是否是鏈表,第二次computeIfAbsent時,不滿足 35 binCount = 1; 36 for (Node<K,V> e = f;; ++binCount) { 37 K ek; V ev; 38 if (e.hash == h && 39 ((ek = e.key) == key || 40 (ek != null && key.equals(ek)))) { 41 val = e.val; 42 break; 43 } 44 Node<K,V> pred = e; 45 if ((e = e.next) == null) { 46 if ((val = mappingFunction.apply(key)) != null) { 47 added = true; 48 pred.next = new Node<K,V>(h, key, val, null); 49 } 50 break; 51 } 52 } 53 } 54 else if (f instanceof TreeBin) { //⑥判斷是否是紅黑樹,第二次computeIfAbsent時不滿足 55 binCount = 2; 56 TreeBin<K,V> t = (TreeBin<K,V>)f; 57 TreeNode<K,V> r, p; 58 if ((r = t.root) != null && 59 (p = r.findTreeNode(h, key, null)) != null) 60 val = p.val; 61 else if ((val = mappingFunction.apply(key)) != null) { 62 added = true; 63 t.putTreeVal(h, key, val); 64 } 65 } 66 } 67 } 68 if (binCount != 0) { //⑦如果當前位置的元素個數大於0,返回 69 if (binCount >= TREEIFY_THRESHOLD) 70 treeifyBin(tab, i); 71 if (!added) 72 return val; 73 break; 74 }
75 } 76 } 77 if (val != null) 78 addCount(1L, binCount); 79 return val; 80 }
分析:
map2.computeIfAbsent("AaAa",。。。)會走①②③邏輯,此時在tab里放了一個占位node
map2.computeIfAbsent("BBB",。。。)會不滿足①②,進入④,因為之前放了一個占位node,
在里面⑤⑥都不滿足,不進入其中邏輯,走⑦也不滿足,循環中沒有挑出的地方死循環。
解決:在jdk9中進行了解決,如果出現嵌套調用computeIfAbsent,並且正好key的hash值沖突,
則在④里面的⑤⑥判斷完是否是鏈表和紅黑樹之后增加判斷是否是占位node,如果是就拋出異常
-
-
IllegalStateException
- 如果計算可檢測地嘗試對此地圖的遞歸更新,否則將永遠不會完成
-