ConcurrentHashMap的一個bug

最近發(fā)現(xiàn)java 1.8的concurrentHashMap,在使用computeIfAbsent時,如果涉及修改map,則會產(chǎn)生bug。
示例代碼如下:

        System.out.println("start.");
        map.computeIfAbsent("t",
                (String t) -> map.computeIfAbsent("t", (String i) -> "i")); //halt在這里
        System.out.println("fin.");

如果執(zhí)行這段代碼,你會發(fā)現(xiàn)代碼會停在注釋出,一直沒有結(jié)果。
最開始以為是遞歸實現(xiàn)的問題,通俗的說,就是在構(gòu)造一個函數(shù)的時候陷入了自遞歸。就是你想構(gòu)造一個A,但是A的構(gòu)造依賴A已完成構(gòu)造后的某些屬性。為了驗證是否是這個原因,我們把代碼做一些調(diào)整,消除遞歸調(diào)用。

        ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
        System.out.println("start.");
        map.computeIfAbsent("t",
                (String t) -> {
                    map.put("t", "t");
                    return "t";
                });
        System.out.println("fin.");

你會發(fā)現(xiàn),代碼繼續(xù)停在哪里,無法輸出"fin."。

然后懷疑是死鎖,懷疑concurrentHashMap使用了非可重入鎖。但是跟著看conrrentHashMap的實現(xiàn),發(fā)現(xiàn)是基于cas + synchronized的方式實現(xiàn),而synchronized本身是可重入的,因此這里不滿足死鎖的條件。
繼續(xù)看concurrentHashMap的注釋,里面有這樣一句話:

/*
  must not attempt to update any other mappings of this map.
*/

這句話確定了這個問題應(yīng)該是已知存在的。
所以應(yīng)該絕對避免在computeIfAbsent中有遞歸,或者修改map的任何操作

為了搞清楚原因,我們繼續(xù)debug concurrentHashMap的源碼,發(fā)現(xiàn)這種在computeIfAbsent中,如果嘗試修改map的情況下,代碼會在

        for (Node<K,V>[] tab = table;;) {  //無限循環(huán)
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {
                Node<K,V> r = new ReservationNode<K,V>();
                synchronized (r) {
                    if (casTabAt(tab, i, null, r)) { //cas
....

中反復(fù)循環(huán)。
我嘗試通俗的解釋一下這個問題:
注:不見得正確,只是個人理解
由于concurrentHashMap中使用的是cas操作,因此在出現(xiàn)cas嵌套的情況下,就會形成一種『死鎖』。舉例來說,一個值原來是 1, 我想把它修改成2,正常的cas操作,會比較在修改的那一刻,值是否仍然為1。這種比較,在cas只有一層的情況下,是沒有問題的。但是,假如有兩層cas,這個值原來是1,第一層把 1 -> 2,在cas還沒有生效時,繼續(xù)進(jìn)入第二層cas操作,把 2 -> 3,當(dāng)最終提交時,第二層cas比較當(dāng)前值是否是2,但由于當(dāng)前指仍然是1,因此修改無效。最終反復(fù)進(jìn)入循環(huán),形成死鎖。

雖然computeIfAbsent的代碼注釋中對這種修改map的行為做了強(qiáng)提示,但在實際中,我認(rèn)為這種行為仍舊是concurrentHashMap的一個實現(xiàn)bug。

https://bugs.openjdk.java.net/browse/JDK-8172951
好在這個問題在java 1.9中已經(jīng)基本修復(fù)了。

This is fixed in JDK 9 with JDK-8071667 . When the test case is run in JDK 9-ea, it gives a ConcurrentModification Exception.
java 9

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容