如何避免死鎖?我們有套路可循

寫在前面

上一篇文章共享資源那么多,如何用一把鎖保護(hù)多個(gè)資源? 文章我們談到了銀行轉(zhuǎn)賬經(jīng)典案例,其中有兩個(gè)問(wèn)題:

  1. 單純的用 synchronized 方法起不到保護(hù)作用(不能保護(hù) target)
  2. 用 Account.class 鎖方案,鎖的粒度又過(guò)大,導(dǎo)致涉及到賬戶的所有操作(取款,轉(zhuǎn)賬,修改密碼等)都會(huì)變成串行操作

如何解決這兩個(gè)問(wèn)題呢?咱們先換好衣服穿越回到過(guò)去尋找一下錢莊,一起透過(guò)現(xiàn)象看本質(zhì),dengdeng deng.......

來(lái)到錢莊,告訴柜員你要給鐵蛋兒轉(zhuǎn) 100 銅錢,這時(shí)柜員轉(zhuǎn)身在墻上尋找你和鐵蛋兒的賬本,此時(shí)柜員可能面臨三種情況:

  1. 理想狀態(tài): 你和鐵蛋兒的賬本都是空閑狀態(tài),一起拿回來(lái),在你的賬本上減 100 銅錢,在鐵蛋兒賬本上加 100 銅錢,柜員轉(zhuǎn)身將賬本掛回到墻上,完成你的業(yè)務(wù)
  2. 尷尬狀態(tài): 你的賬本在,鐵蛋兒的賬本被其他柜員拿出去給別人轉(zhuǎn)賬,你要等待其他柜員把鐵蛋兒的賬本歸還
  3. 抓狂狀態(tài): 你的賬本不在,鐵蛋兒的賬本也不在,你只能等待兩個(gè)賬本都?xì)w還

放慢柜員的取賬本操作,他一定是先拿到你的賬本,然后再去拿鐵蛋兒的賬本,兩個(gè)賬本都拿到(理想狀態(tài))之后才能完成轉(zhuǎn)賬,用程序模型來(lái)描述一下這個(gè)拿取賬本的過(guò)程:

我們繼續(xù)用程序代碼描述一下上面這個(gè)模型:

class Account {
  private int balance;
  // 轉(zhuǎn)賬
  void transfer(Account target, int amt){
    // 鎖定轉(zhuǎn)出賬戶
    synchronized(this) {              
      // 鎖定轉(zhuǎn)入賬戶
      synchronized(target) {           
        if (this.balance > amt) {
          this.balance -= amt;
          target.balance += amt;
        }
      }
    }
  } 
}

這個(gè)解決方案看起來(lái)很完美,解決了文章開(kāi)頭說(shuō)的兩個(gè)問(wèn)題,但真是這樣嗎?


我們剛剛說(shuō)過(guò)的理想狀態(tài)是錢莊只有一個(gè)柜員(既單線程)。隨著錢莊規(guī)模變大,墻上早已掛了非常多個(gè)賬本,錢莊為了應(yīng)對(duì)繁忙的業(yè)務(wù),開(kāi)通了多個(gè)窗口,此時(shí)有多個(gè)柜員(多線程)處理錢莊業(yè)務(wù)。

柜員 1 正在辦理給鐵蛋兒轉(zhuǎn)賬的業(yè)務(wù),但只拿到了你的賬本;柜員 2 正在辦理鐵蛋兒給你轉(zhuǎn)賬的業(yè)務(wù),但只拿到了鐵蛋兒的賬本,此時(shí)雙方出現(xiàn)了尷尬狀態(tài),兩位柜員都在等待對(duì)方歸還賬本為當(dāng)前客戶辦理轉(zhuǎn)賬業(yè)務(wù)。

[圖片上傳失敗...(image-d1e7b7-1572310564998)]

現(xiàn)實(shí)中柜員會(huì)溝通,喊出一嗓子 老鐵,鐵蛋兒的賬本先給我用一下,用完還給你,但程序卻沒(méi)這么智能,synchronized 內(nèi)置鎖非常執(zhí)著,它會(huì)告訴你「死等」的道理,最終出現(xiàn)死鎖

Java 有了 synchronized 內(nèi)置鎖,還發(fā)明了顯示鎖 Lock,是不是就為了治一治 synchronized 「死等」的執(zhí)著呢???

解決方案

如何解決上面的問(wèn)題呢?正所謂知己知彼方能百戰(zhàn)不殆,我們要先了解什么情況會(huì)發(fā)生死鎖,才能知道如何避免死鎖,很幸運(yùn)我們可以站在巨人的肩膀上看待問(wèn)題

Coffman 總結(jié)出了四個(gè)條件說(shuō)明可以發(fā)生死鎖的情形:

Coffman 條件

互斥條件:指進(jìn)程對(duì)所分配到的資源進(jìn)行排它性使用,即在一段時(shí)間內(nèi)某資源只由一個(gè)進(jìn)程占用。如果此時(shí)還有其它進(jìn)程請(qǐng)求資源,則請(qǐng)求者只能等待,直至占有資源的進(jìn)程用畢釋放。

請(qǐng)求和保持條件:指進(jìn)程已經(jīng)保持至少一個(gè)資源,但又提出了新的資源請(qǐng)求,而該資源已被其它進(jìn)程占有,此時(shí)請(qǐng)求進(jìn)程阻塞,但又對(duì)自己已獲得的其它資源保持不放。

不可剝奪條件:指進(jìn)程已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時(shí)由自己釋放。

環(huán)路等待條件:指在發(fā)生死鎖時(shí),必然存在一個(gè)進(jìn)程——資源的環(huán)形鏈,即進(jìn)程集合{P1,P2,···,Pn}中的 P1 正在等待一個(gè) P2 占用的資源;P2 正在等待 P3 占用的資源,……,Pn 正在等待已被 P0 占用的資源。

這幾個(gè)條件很好理解,其中「互斥條件」是并發(fā)編程的根基,這個(gè)條件沒(méi)辦法改變。但其他三個(gè)條件都有改變的可能,也就是說(shuō)破壞另外三個(gè)條件就不會(huì)出現(xiàn)上面說(shuō)到的死鎖問(wèn)題

破壞請(qǐng)求和保持條件

每個(gè)柜員都可以取放賬本,很容易出現(xiàn)互相等待的情況。要想破壞請(qǐng)求和保持條件,就要一次性拿到所有資源。

作為程序猿你一定聽(tīng)過(guò)這句話:

任何軟件工程遇到的問(wèn)題都可以通過(guò)增加一個(gè)中間層來(lái)解決

我們不允許柜員都可以取放賬本,賬本要由單獨(dú)的賬本管理員來(lái)管理

也就是說(shuō)賬本管理員拿取賬本是臨界區(qū),如果只拿到其中之一的賬本,那么不會(huì)給柜員,而是等待柜員下一次詢問(wèn)是否兩個(gè)賬本都在

//賬本管理員
public class AccountBookManager {
    synchronized boolean getAllRequiredAccountBook( Object from, Object to){
        if(拿到所有賬本){
            return true;
        } else{
            return false;
        }
    }
    // 歸還資源
    synchronized void releaseObtainedAccountBook(Object from, Object to){
        歸還獲取到的賬本
    }
}


public class Account {
    //單例的賬本管理員
    private AccountBookManager accountBookManager;

    public void transfer(Account target, int amt){
        // 一次性申請(qǐng)轉(zhuǎn)出賬戶和轉(zhuǎn)入賬戶,直到成功
        while(!accountBookManager.getAllRequiredAccountBook(this, target)){
            return;
        }

        try{
            // 鎖定轉(zhuǎn)出賬戶
            synchronized(this){
                // 鎖定轉(zhuǎn)入賬戶
                synchronized(target){
                    if (this.balance > amt){
                        this.balance -= amt;
                        target.balance += amt;
                    }
                }
            }
        } finally {
            accountBookManager.releaseObtainedAccountBook(this, target);
        }
    }
}

破壞不可剝奪條件

上面已經(jīng)給了你小小的提示,為了解決內(nèi)置鎖的執(zhí)著,Java 顯示鎖支持通知(notify/notifyall)和等待(wait),也就是說(shuō)該功能可以實(shí)現(xiàn)喊一嗓子 老鐵,鐵蛋兒的賬本先給我用一下,用完還給你 的功能,這個(gè)后續(xù)將到 Java SDK 相關(guān)內(nèi)容時(shí)會(huì)做說(shuō)明

破壞環(huán)路等待條件

破壞環(huán)路等待條件也很簡(jiǎn)單,我們只需要將資源序號(hào)大小排序獲取就會(huì)解決這個(gè)問(wèn)題,將環(huán)路拆除

繼續(xù)用代碼來(lái)說(shuō)明:

class Account {
  private int id;
  private int balance;
  // 轉(zhuǎn)賬
  void transfer(Account target, int amt){
    Account smaller = this        
    Account larger = target;    
    // 排序
    if (this.id > target.id) { 
      smaller = target;           
      larger = this;            
    }                          
    // 鎖定序號(hào)小的賬戶
    synchronized(smaller){
      // 鎖定序號(hào)大的賬戶
      synchronized(larger){ 
        if (this.balance > amt){
          this.balance -= amt;
          target.balance += amt;
        }
      }
    }
  } 
}

當(dāng) smaller 被占用時(shí),其他線程就會(huì)被阻塞,也就不會(huì)存在死鎖了.

附加說(shuō)明

在實(shí)際業(yè)務(wù)中,關(guān)于 Account 都會(huì)是數(shù)據(jù)庫(kù)對(duì)象,我們可以通過(guò)事務(wù)或數(shù)據(jù)庫(kù)的樂(lè)觀鎖來(lái)解決的。另外分布式系統(tǒng)中,賬本管理員這個(gè)角色的處理也可能會(huì)用 redis 分布式鎖來(lái)解決.

在處理破壞請(qǐng)求和保持條件時(shí),我們使用的是 while 循環(huán)方式來(lái)不斷請(qǐng)求鎖的時(shí)候,在實(shí)際業(yè)務(wù)中,我們會(huì)有 timeout 的設(shè)置,防止無(wú)休止的浪費(fèi) CPU 使用率

另外大家可以嘗試使用阿里開(kāi)源工具 Arthas 來(lái)查看 CPU 使用率,線程等相關(guān)問(wèn)題,github 上有明確的說(shuō)明

總結(jié)

計(jì)算機(jī)的計(jì)算能力遠(yuǎn)遠(yuǎn)超過(guò)人類,但是他的智慧還需要有帶提高,當(dāng)看待并發(fā)問(wèn)題時(shí),我們往往認(rèn)為人類的最基本溝通計(jì)算機(jī)也可以做到,其實(shí)不然,還是那句話,編寫并發(fā)程序,要站在計(jì)算機(jī)的角度來(lái)看待問(wèn)題

粗粒度鎖我們不提倡,所以會(huì)使用細(xì)粒度鎖,但使用細(xì)粒度鎖的時(shí)候,我們要嚴(yán)格按照 Coffman 的四大條件來(lái)逐條判斷,這樣再應(yīng)用我們這幾個(gè)解決方案來(lái)解決就好了

靈魂追問(wèn)

  1. 破壞請(qǐng)求和保持條件時(shí),處理能力的瓶頸在賬本管理員那里,那你覺(jué)得這種處理方式會(huì)提高并發(fā)量嗎?
  2. 破壞請(qǐng)求保持條件的方法和破壞環(huán)路等待的方法,你覺(jué)得那種方式更好
  3. 破壞請(qǐng)求和保持條件時(shí),如果代碼換成下面的樣子會(huì)發(fā)生什么?
public void transfer(Account target, int amt){
    // 一次性申請(qǐng)轉(zhuǎn)出賬戶和轉(zhuǎn)入賬戶,直到成功
    while(accountBookManager.getAllRequiredAccountBook(this, target)){}
        try{
            // 鎖定轉(zhuǎn)出賬戶
            synchronized(this){
                // 鎖定轉(zhuǎn)入賬戶
                synchronized(target){
                    if (this.balance > amt){
                        this.balance -= amt;
                        target.balance += amt;
                    }
                }
            }
        } finally {
            accountBookManager.releaseObtainedAccountBook(this, target);
        }
    }
}

提高效率工具


  1. 這次走進(jìn)并發(fā)的世界,請(qǐng)不要錯(cuò)過(guò)
  2. 學(xué)并發(fā)編程,透徹理解這三個(gè)核心是關(guān)鍵
  3. 并發(fā)Bug之源有三,請(qǐng)睜大眼睛看清它們
  4. 可見(jiàn)性有序性,Happens-before來(lái)搞定
  5. 解決原子性問(wèn)題?你首先需要的是宏觀理解
  6. 面試并發(fā)volatile關(guān)鍵字時(shí),我們應(yīng)該具備哪些談資?

歡迎持續(xù)關(guān)注公眾號(hào):「日拱一兵」

  • 前沿 Java 技術(shù)干貨分享
  • 高效工具匯總 | 回復(fù)「工具」
  • 面試問(wèn)題分析與解答
  • 技術(shù)資料領(lǐng)取 | 回復(fù)「資料」

以讀偵探小說(shuō)思維輕松趣味學(xué)習(xí) Java 技術(shù)棧相關(guān)知識(shí),本著將復(fù)雜問(wèn)題簡(jiǎn)單化,抽象問(wèn)題具體化和圖形化原則逐步分解技術(shù)問(wèn)題,技術(shù)持續(xù)更新,請(qǐng)持續(xù)關(guān)注......

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容