一道阿里多執行緒面試題分析
首先,來看看這個面試題目吧。
題目來源: http://www.linuxidc.com/Linux/2014-03/98715.htm
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); } } }
問題: 這段程式碼大多數情況下執行正常,但是某些情況下會出問題。什麼時候會出現什麼問題?如何修正?
可以看出,MyStack主要實現入棧出棧功能,ArrayList不是執行緒安全的類,因此程式中用synchronized關鍵字來保證執行緒安全。大多數情況下,都能正確執行,但是在特殊情況下會出現一些意外。
tips:從功能上來說wait就是說執行緒在獲取物件鎖後,主動釋放物件鎖,同時本執行緒休眠。直到有其它執行緒呼叫物件的notify()喚醒該執行緒,才能繼續獲取物件鎖,並繼續執行。相應的notify()就是對物件鎖的喚醒操作。但有一點需要注意的是notify()呼叫後,並不是馬上就釋放物件鎖的,而是在相應的synchronized(){}語句塊執行結束,自動釋放鎖後,JVM會在wait()物件鎖的執行緒中隨機選取一執行緒,賦予其物件鎖,喚醒執行緒,繼續執行。這樣就提供了線上程間同步、喚醒的操作。Thread.sleep()與Object.wait()二者都可以暫停當前執行緒,釋放CPU控制權,主要的區別在於Object.wait()在釋放CPU同時,釋放了物件鎖的控制。
case1:刪除不存在的元素
假設現在有三個執行緒A、B、C,其中A用於新增元素,B、C用於刪除元素。
某時刻,棧為空,
step1、執行緒B執行,獲取鎖,list.size()=0,進入wait(),wait狀態下會釋放當前鎖
step2、執行緒A執行,獲取鎖,新增元素,執行list.add(value),此時list.size()=1,注意:在A執行notify()之前,執行緒C啟動,發現其他執行緒已經擁有物件鎖,因此進入阻塞狀態,等待鎖
step3、執行緒A執行notify(),試圖喚醒等待中的執行緒B,但是但是但是,如果此時C獲取了物件鎖,那麼將優先執行,那麼C判斷list.size()=1,直接刪除元素,然後釋放物件鎖
step4、wait狀態下的B獲取物件鎖,直接執行list.remove(list.size()-1),發生錯誤!!!
解決辦法: 使用可同步的資料結構來存放資料,比如LinkedBlockingQueue之類。由這些同步的資料結構來完成繁瑣的同步操作。
case2:虛假喚醒
虛假喚醒就是一些obj.wait()會在除了obj.notify()和obj.notifyAll()的其他情況被喚醒,而此時是不應該喚醒的。
解決的辦法是基於while來反覆判斷進入正常操作的臨界條件是否滿足: (將if換成while)
synchronized (obj) {
while (<condition does not hold>)
obj.wait();
... // Perform action appropriate to condition
}