引言: 通過多線程的面試題目分析,來深入理解Java線程的狀態轉變過程。
最近在學習Java多線程設計的時候,在網上看到一個面試題目的討論,雖然樓主所說有些道理,但感覺還是有些問題,故此在和同事討論以後還是有了若干收獲,在此略作總結。
首先,來看看這個面試題目吧。
public class MyStack {
private List<String> list = new ArrayList<String>();
public synchronized void push(String value) {
synchronized (this) {
list.add(value);
notify();
}
}
public synchronized String pop() throws InterruptedException {
synchronized (this) {
if (list.size() <= 0) {
wait();
}
return list.remove(list.size() - 1);
}
}
}
問題: 這段代碼大多數情況下運行正常,但是某些情況下會出問題。什麼時候會出現什麼問題?如何修正?
代碼分析:
從整體上,在並發狀態下,push和pop都使用了synchronized的鎖,來實現同步,同步的數據對象是基於List的數據;大部分情況下是可以正常工作的。
問題描述:
狀況1:
1. 假設有三個線程: A,B,C. A 負責放入數據到list,就是調用push操作, B,C分別執行Pop操作,移除數據。
2. 首先B先執行,於pop中的wait()方法處,進入waiting狀態,進入等待隊列,釋放鎖。
3. A首先執行放入數據push操作到List,在調用notify()之前; 同時C執行pop(),由於synchronized,被阻塞,進入Blocked狀態,放入基於鎖的等待隊列。注意,這裡的隊列和2中的waiting等待隊列是兩個不同的隊列。
4. A線程調用notify(),喚醒等待中的線程A。
5. 如果此時, C獲取到基於對象的鎖,則優先執行,執行pop方法,獲取數據,從list移除一個元素。
6. 然後,A獲取到競爭鎖,A中調用list.remove(list.size() - 1),則會報數據越界exception。
狀況2:
1. 相同於狀況1
2. B、C都處於等待waiting狀態,釋放鎖。等待notify()、notifyAll()操作的喚醒。
3. 存在被虛假喚醒的可能。
何為虛假喚醒?
虛假喚醒就是一些obj.wait()會在除了obj.notify()和obj.notifyAll()的其他情況被喚醒,而此時是不應該喚醒的。
解決的辦法是基於while來反復判斷進入正常操作的臨界條件是否滿足:
synchronized (obj) {
while (<condition does not hold>)
obj.wait();
... // Perform action appropriate to condition
}
如何修復問題?
#1. 使用可同步的數據結構來存放數據,比如LinkedBlockingQueue之類。由這些同步的數據結構來完成繁瑣的同步操作。
#2. 雙層的synchronized使用沒有意義,保留外層即可。
#3. 將if替換為while,解決虛假喚醒的問題。