最近發(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