- 現象
- 原因
- 解決
現象
大家可能都聽過JDK7中的HashMap在多執行緒環境下可能造成CPU 100%的現象,這個由於在擴容的時候put時產生了死鏈,由此會在get時造成了CPU 100%。這個問題在JDK8中的HashMap獲得瞭解決。其實JDK7中的HashMap在多執行緒環境下不止只有CPU 100%這一共怪異現象,它還可能造成插入的資料丟失,有興趣的讀者可以自行瞭解下。
對於HashMap多執行緒的問題,我們通常會這麼反問:HashMap設計上就不是多執行緒安全的,何必要去在多執行緒環境下用呢?的確如此,我們不會傻到顯式的在多執行緒環境下呼叫,但是又可能在你所關註的視角範圍外是多執行緒的,其隱式地讓HashMap置於多執行緒環境下了,這個又難以一下子察覺到。再者,對於HashMap多執行緒的問題,我們很多時候推薦使用ConcurrentHashMap來代替HashMap應用於多執行緒的環境,很不巧的是ConcurrentHashMap也有可能會造成CPU 100%的異常現象。這個怪異現象存在於JDK8的ConcurrentHashMap中,在JDK9中已經得到修複,可以參見:https://bugs.openjdk.java.net/browse/JDK-8062841
什麼情況下JDK8的ConcurrentHashMap會出現這個Bug呢?首先我們來執行一下這段程式碼:
map.computeIfAbsent("AaAa",
(String key) -> {
map.put("BBBB", "value");
return "value";
});
你會驚奇的發現這個程式一直處於Running狀態,我們透過top -Hp [pid]命令檢視到其中一個執行緒的CPU使用率接近100%,參考下圖:
可以看到pid為31417的東東,我們再透過jstack -l [pid]命令檢視到對應的執行緒為:
註意將nid=0x7ab9的16進位制轉為10進位制就是31417。可以看到問題是發生在了computeIfAbsent方法中,我們將示例中的程式換成下麵這段程式也會同樣出現CPU 100%的Bug:
static Map concurrentMap = new ConcurrentHashMap<>();
public static void main(String[] args) {
System.out.println("Fibonacci result for 20 is" + fibonacci(20));
}
static int fibonacci(int i) {
if (i == 0)
return i;
if (i == 1)
return 1;
return concurrentMap.computeIfAbsent(i, (key) -> {
System.out.println("Value is " + key);
return fibonacci(i - 2) + fibonacci(i - 1);
});
}
至於為什麼會發生這個BUG,答案就在ConcurrentHashMap中的computeIfAbsent方法中,自己去撈吧,嘿嘿。
原因
map.computeIfAbsent(key1, mappingFunction)
如果當前key1-hash對應的tab位(可以理解為槽)剛好是空的,在計算mappingFunction之前會
- step1: 先往對應位置放一個ReservationNode佔位
- step2: 然後計算mappingFunction的值value,
- step3: 再將value組裝成最終NODE, 把佔位的ReservationNode換成最終NODE;
這時如果:
mappingFunction 中用到了 當前map的computeIfAbsent方法, 很不巧 key2-hash的槽為和key1的是同一個,
因為key1已經在槽中放入了佔位節點, 在處理key2時候for迴圈的所以處理條件都不符合 程式進入了死迴圈
但是如果:
key2-hash的槽位和key1的不一樣, 是不會發生死迴圈
多執行緒問題:
因為ConcurrentHashMap在處理上述step1-step3是同步的, 而且在處理時候會同步獲取的值, 所以是不存在執行緒不安全的, 純粹是當前執行緒死迴圈
Thread1 透過cas 在槽x放了個ReservationNode(RN1), 然後假設mappingFunction執行的很慢
Thread2 在槽x和Thread競爭, cas失敗沒有搶到佔位符; 進行下一輪for迴圈, 這是因為槽x中已經被放置了RN1, 所以Thread2獲取到這個RN1,在執行synchronized(RN1) 時候被thread1block住
code sample:
public static void main(String[] args) throws IOException {
ConcurrentHashMap map = new ConcurrentHashMap<>();
//caseA: dead loop
// map.computeIfAbsent("AA", key -> map.computeIfAbsent("BB", k->"bb"));
//caseB: block, but no dead loop
new Thread(()->map.computeIfAbsent("AA", key -> waitAndGet())).start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(3); //delay 1 second
} catch (InterruptedException e) {}
map.computeIfAbsent("BB", key-> "bb");
}).start();
}
private static String waitAndGet(){
try {
TimeUnit.SECONDS.sleep(20);
} catch (InterruptedException e) {
}
return "AAA";
}
解決
怎麼規避這個問題呢?只要不在遞迴中使用computeIfAbsent方法就好啦,或者降級用可愛的分段鎖,或者升級JDK9~