掃盲貼:死鎖、活鎖、飢餓

在安全性與活躍性之間通常存在著某種制衡,我們使用加鎖機制來保證線程安全,但如果過度使用加鎖,則可能導致死鎖。

死鎖

在業界有一個經典的哲學家進餐問題很好的描述了死鎖狀況。5個哲學家去吃中餐,他們有5根筷子,每兩個人中間放一根筷子,哲學家們時而思考,時而進餐。每個人需要一雙筷子才能吃到東西,並在吃完東西后,將筷子放回原處,繼續思考。有些筷子管理算法能讓每個人都能吃到東西,例如:一個哲學家會嘗試獲得相鄰的兩根筷子,如果其中一根筷子被另一個哲學家使用,他就會放棄手中獲得那根筷子,繼續等待幾分鐘之後再次嘗試;但是有些管理算法會導致所有哲學家餓死,例如:每個人都死死抓住自己的筷子,然後等待另一根筷子,這將產生死鎖。

當一個線程永遠的持有一個鎖,並且其他線程都嘗試獲得這個鎖時,那麼它們將永遠被阻塞,在線程A持有鎖M,又在嘗試獲取鎖,同時線程B持有鎖L ,又在嘗試鎖M ,那麼線程A和線程B都將永遠的等待下去,這種情況是最簡單的死鎖形式。

在數據庫的設計中考慮了檢測死鎖以及從死鎖中恢復,在執行一個事務時可能需要多個鎖,並一直持有這些鎖直到事務提交,因此在兩個事務之間很可能發生死鎖,但事實上這種情況並不常見,原因是數據庫服務器檢測到死鎖時,會選擇一個犧牲者,放棄這個事務,作為犧牲者的事務會釋放鎖,從而使其他事務正常執行,當其他事務都完成的時候,程序會重新啟動這個犧牲者繼續完成事務。

顯然JVM解決死鎖問題方面沒有數據庫那樣強大,當Java線程發生死鎖的時候,這些線程將再也無法繼續使用了。

1、鎖順序導致死鎖

我們來看一段代碼

 1public class LockDemo { 2 private Object left = new Object(); 3 private Object right = new Object(); 4 5 public void leftRight(){ 6 synchronized (left){ 7 synchronized (right){ 8 //執行相關業務代碼 9 dosomthing();10 }11 }12 }131415 public void rightLeft(){1617 synchronized (right){18 synchronized (left){19 //執行相關業務代碼20 dosomthing();21 }22 }23 }24}

leftRight 和rightLeft方法分別獲得left和right 鎖,如果一個線程調用leftRight方法,並且同時另外一個線程調用了rightLeft方法,那麼會發生死鎖;

掃盲貼:死鎖、活鎖、飢餓

2、動態的鎖順序死鎖

有時候通過程序代碼,並不能清楚的知道是否通過鎖順序來避免死鎖的發生。我們看下面一段代碼,他將資金從一個賬戶轉入到另一個賬戶,在開始轉賬之前需要獲得這兩個Account對象的鎖。

 1 public void transfer(Account from,Account to,Amount amount){ 2 synchronized (from){ 3 synchronized (to){ 4 //... 此處省略一些校驗信息 5 //from 賬戶扣錢 6 from.debit(amount); 7 8 //to 賬戶加錢 9 to.credit(amount);10 }11 }12 }

這段代碼如何發生死鎖呢?如果兩個線程同時調用transfer ,其中一個線程從X向Y轉賬,另外一個線程從Y向X轉賬,那麼將會發生死鎖。

1A: transfer(youAccount,myAccount,10);2B: transfer(myAccount,youAccount,20);

3、如何避免死鎖

3.1 只加一個鎖

如果一個程序每次最多隻獲取一個鎖,那麼就不會產生死鎖,當然這種情況不現實,如果能避免每次獲取多個鎖,則可以省去很多工作。如果必須獲取多個鎖,在設計的時候需要考慮鎖的順序,並且儘量減少加鎖的交互數量;

3.2 支持定時的鎖

我們可以使用AQS定製一種定時的鎖,如果沒有獲得鎖,我們設置一個超時時限,在等待超時時限後,我們返回一個timeout失敗信息,如果超時時限比獲取鎖的時間要長很多,可以自己重新嘗試獲取鎖的控制權。

飢餓

當線程由於無法訪問它所需要的資源而不能繼續執行的時候,就發生了“飢餓”。引發飢餓的最常見的資源就是CPU時鐘週期。如果在JAVA程序中對線程優先級使用不當,或者在持有鎖時執行一些無法結束的結構(例如:無限循環,或者無限制的等待某個資源),那麼就會導致飢餓,因為其他線程無法獲取這個鎖。

活鎖

活鎖(Livelock)是另一種形式的話躍性問題,該問題儘管不會阻塞線程,但也不能繼續執行,因為線程將不斷重複執行相同的操作,而且總會失敗。活鎖通常發生在處理事務消息的應用程序中:如果不能成功地處理某個消息,那麼消息處理機制將回滾整個事務,並將它重新放到隊列的開頭,線程將不斷重複執行相同的操作,而且總會失敗。

當多個相互協作的線程都對彼此進行響應從而修改各自的狀態,並使得任何一個線程都無法繼續執行時,就發生了活鎖。這就像兩個過於禮貌的人在半路上面對面地相遇:他們彼此都讓出對方的路,然而又在另一條路上相遇了。因此他們就這樣反覆地避讓下去。

要解決這種活鎖問題,需要在重試機制中引人隨機性。例如:在網絡上,如果兩臺機器嘗試使用相同的載波來發送數據包,那麼這些數據包將會發生衝突,這兩臺機器檢測到了衝突後都選擇了1秒後重發,那麼衝突就會永遠進行下去,如果引入隨機時間進行重試,那麼將避免衝突,從而有效的避免活鎖。

本頭條號團隊成員由餓了麼、阿里、螞蟻金服等同事組成,另外可以關注V信公眾號: jiagoushizhidian,可以瞭解最前沿的技術。


分享到:


相關文章: