1. 程式人生 > >【高併發】優化加鎖方式時竟然死鎖了!!

【高併發】優化加鎖方式時竟然死鎖了!!

寫在前面

今天,在優化程式的加鎖方式時,竟然出現了死鎖!!到底是為什麼呢?!經過仔細的分析之後,終於找到了原因。

為何需要優化加鎖方式?

在《【高併發】高併發環境下詭異的加鎖問題(你加的鎖未必安全)》一文中,我們在轉賬類TansferAccount中使用TansferAccount.class物件對程式加鎖,如下所示。

public class TansferAccount{
    private Integer balance;
    public void transfer(TansferAccount target, Integer transferMoney){
        synchronized(TansferAccount.class){
        	if(this.balance >= transferMoney){
                this.balance -= transferMoney;
                target.balance += transferMoney;
            }   
        }
    }
}

這種方式確實解決了轉賬操作的併發問題,但是這種方式在高併發環境下真的可取嗎?試想,如果我們在高併發環境下使用上述程式碼來處理轉賬操作,因為TansferAccount.class物件是JVM在載入TansferAccount類的時候建立的,所有的TansferAccount例項物件都會共享一個TansferAccount.class物件。也就是說,所有TansferAccount例項物件執行transfer()方法時,都是互斥的!!換句話說,所有的轉賬操作都是序列的!!

如果所有的轉賬操作都是序列執行的話,造成的後果就是:賬戶A為賬戶B轉賬完成後,才能進行賬戶C為賬戶D的轉賬操作。如果全世界的網民一起執行轉賬操作的話,這些轉賬操作都序列執行,那麼,程式的效能是完全無法接受的!!!

其實,賬戶A為賬戶B轉賬的操作和賬戶C為賬戶D轉賬的操作完全可以並行執行。所以,我們必須優化加鎖方式,提升程式的效能!!

初步優化加鎖方式

既然直接TansferAccount.class對程式加鎖在高併發環境下不可取,那麼,我們到底應該怎麼做呢?!

仔細分析下上面的程式碼業務,上述程式碼的轉賬操作中,涉及到轉出賬戶this和轉入賬戶target,所以,我們可以分別對轉出賬戶this和轉入賬戶target加鎖,只有兩個賬戶加鎖都成功時,才執行轉賬操作。這樣就能夠做到賬戶A為賬戶B轉賬的操作和賬戶C為賬戶D轉賬的操作完全可以並行執行。

我們可以將優化後的邏輯用下圖表示。

根據上面的分析,我們可以將TansferAccount的程式碼優化成如下所示。

public class TansferAccount{
    //賬戶的餘額
    private Integer balance;
    //轉賬操作
    public void transfer(TansferAccount target, Integer transferMoney){
        //對轉出賬戶加鎖
        synchronized(this){
            //對轉入賬戶加鎖
            synchronized(target){
                if(this.balance >= transferMoney){
                    this.balance -= transferMoney;
                    target.balance += transferMoney;
                }   
            }
        }
    }
}

此時,上面的程式碼看上去沒啥問題,但真的是這樣嗎? 我也希望程式是完美的,但是往往卻不是我們想的那樣啊!沒錯,上面的程式會出現 死鎖, 為什麼會出現死鎖啊? 接下來,我們就開始分析一波。

死鎖的問題分析

TansferAccount類中的程式碼看上去比較完美,但是優化後的加鎖方式竟然會導致死鎖!!!這是我親測得出的結論!!

關於死鎖我們可以結合改進的TansferAccount類舉一個簡單的場景:假設有執行緒A和執行緒B兩個執行緒同時執行在兩個不同的CPU上,執行緒A執行賬戶A向賬戶B轉賬的操作,執行緒B執行賬戶B向賬戶A轉賬的操作。當執行緒A和執行緒B執行到 synchronized(this)程式碼時,執行緒A獲得了賬戶A的鎖,執行緒B獲得了賬戶B的鎖。當執行到synchronized(target)程式碼時,執行緒A嘗試獲得賬戶B的鎖時,發現賬戶B已經被執行緒B鎖定,此時執行緒A開始等待執行緒B釋放賬戶B的鎖;而執行緒B嘗試獲得賬戶A的鎖時,發現賬戶A已經被執行緒A鎖定,此時執行緒B開始等待執行緒A釋放賬戶A的鎖。

這樣,執行緒A持有賬戶A的鎖並等待執行緒B釋放賬戶B的鎖,執行緒B持有賬戶B的鎖並等待執行緒A釋放賬戶A的鎖,死鎖發生了!!

死鎖的必要條件

在如何解決死鎖之前,我們先來看下發生死鎖時有哪些必要的條件。如果要發生死鎖,則必須存在以下四個必要條件,四者缺一不可。

  • 互斥條件

在一段時間內某資源僅為一個執行緒所佔有。此時若有其他執行緒請求該資源,則請求執行緒只能等待。

  • 不可剝奪條件

執行緒所獲得的資源在未使用完畢之前,不能被其他執行緒強行奪走,即只能由獲得該資源的執行緒自己來釋放(只能是主動釋放)。

  • 請求與保持條件

執行緒已經保持了至少一個資源,但又提出了新的資源請求,而該資源已被其他執行緒佔有,此時請求執行緒被阻塞,但對自己已獲得的資源保持不放。

  • 迴圈等待條件

既然死鎖的發生必須存在上述四個條件,那麼,大家是不是就能夠想到如何預防死鎖了呢?

死鎖的預防

併發程式設計中,一旦發生了死鎖的現象,則基本沒有特別好的解決方法,一般情況下只能重啟應用來解決。因此,解決死鎖的最好方法就是預防死鎖。

發生死鎖時,必然會存在死鎖的四個必要條件。也就是說,如果我們在寫程式時,只要“破壞”死鎖的四個必要條件中的一個,就能夠避免死鎖的發生。接下來,我們就一起來探討下如何“破壞”這四個必要條件。

  • 破壞互斥條件

互斥條件是我們沒辦法破壞的,因為我們使用鎖為的就是執行緒之間的互斥。這一點需要特別注意!!!!

  • 破壞不可剝奪條件

破壞不可剝奪的條件的核心就是讓當前執行緒自己主動釋放佔有的資源,關於這一點,synchronized是做不到的,我們可以使用java.util.concurrent包下的Lock來解決。此時,我們需要將TansferAccount類的程式碼修改成類似如下所示。

public class TansferAccount{
    private Lock thisLock = new ReentrantLock();
    private Lock targetLock = new ReentrantLock();
    //賬戶的餘額
    private Integer balance;
    //轉賬操作
    public void transfer(TansferAccount target, Integer transferMoney){
        boolean isThisLock = thisLock.tryLock();
        if(isThisLock){
            try{
                boolean isTargetLock = targetLock.tryLock();
                if(isTargetLock){
                    try{
                         if(this.balance >= transferMoney){
                            this.balance -= transferMoney;
                            target.balance += transferMoney;
                        }   
                    }finally{
                        targetLock.unlock
                    }
                }
            }finally{
                thisLock.unlock();
            }
        }
    }
}

其中Lock中有兩個tryLock方法,分別如下所示。

  • tryLock()方法

tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他執行緒獲取),則返回false,也就說這個方法無論如何都會立即返回。在拿不到鎖時不會一直在那等待。

  • tryLock(long time, TimeUnit unit)方法

tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false。如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。

  • 破壞請求與保持條件

破壞請求與保持條件,我們可以一次性申請所需要的所有資源,例如在我們完成轉賬操作的過程中,我們一次性申請賬戶A和賬戶B,兩個賬戶都申請成功後,再執行轉賬的操作。此時,我們需要再建立一個申請資源的類ResourcesRequester,這個類的作用就是申請資源和釋放資源。同時,TansferAccount類中需要持有一個ResourcesRequester類的單例物件,當我們需要執行轉賬操作時,首先向ResourcesRequester同時申請轉出賬戶和轉入賬戶兩個資源,申請成功後,再鎖定兩個資源;當轉賬操作完成後,釋放鎖並釋放ResourcesRequester類申請的轉出賬戶和轉入賬戶資源。

ResourcesRequester類的程式碼如下所示。

public class ResourcesRequester{
    //存放申請資源的集合
    private List<Object> resources = new ArrayList<Object>();
    //一次申請所有的資源
    public synchronized boolean applyResources(Object source, Object target){
        if(resources.contains(source) || resources.contains(target)){
            return false;
        }
        resources.add(source);
        resources.add(targer);
        return true;
    }
    
    //釋放資源
    public synchronized void releaseResources(Object source, Object target){
        resources.remove(source);
        resources.remove(target);
    }
}

此時,TansferAccount類的程式碼如下所示。

public class TansferAccount{
    //賬戶的餘額
    private Integer balance;
    //ResourcesRequester類的單例物件
    private ResourcesRequester requester;
   
    //轉賬操作
    public void transfer(TansferAccount target, Integer transferMoney){
        //自旋申請轉出賬戶和轉入賬戶,直到成功
        while(!requester.applyResources(this, target)){
            //迴圈體為空
            ;
        }
        try{
            //對轉出賬戶加鎖
            synchronized(this){
                //對轉入賬戶加鎖
                synchronized(target){
                    if(this.balance >= transferMoney){
                        this.balance -= transferMoney;
                        target.balance += transferMoney;
                    }   
                }
            }
        }finally{
            //最後釋放賬戶資源
            requester.releaseResources(this, target);
        }

    }
}
  • 破壞迴圈等待條件

破壞迴圈等待條件,則可以通過對資源排序,按照一定的順序來申請資源,然後按照順序來鎖定資源,可以有效的避免死鎖。

例如,在我們的轉賬操作中,往往每個賬戶都會有一個唯一的id值,我們在鎖定賬戶資源時,可以按照id值從小到大的順序來申請賬戶資源,並按照id從小到大的順序來鎖定賬戶,此時,程式就不會再進行迴圈等待了。

程式程式碼如下所示。

public class TansferAccount{
    //賬戶的id
    private Integer id;
    //賬戶的餘額
    private Integer balance;
    //轉賬操作
    public void transfer(TansferAccount target, Integer transferMoney){
        TansferAccount beforeAccount = this;
        TansferAccount afterAccount = target;
        if(this.id > target.id){
            beforeAccount = target;
            afterAccount = this;
        }
        //對轉出賬戶加鎖
        synchronized(beforeAccount){
            //對轉入賬戶加鎖
            synchronized(afterAccount){
                if(this.balance >= transferMoney){
                    this.balance -= transferMoney;
                    target.balance += transferMoney;
                }   
            }
        }
    }
}

總結

在併發程式設計中,使用細粒度鎖來鎖定多個資源時,要時刻注意死鎖的問題。另外,避免死鎖最簡單的方法就是阻止迴圈等待條件,將系統中所有的資源設定標誌位、排序,規定所有的執行緒申請資源必須以一定的順序來操作進而避免死鎖。

寫在最後

如果覺得文章對你有點幫助,請微信搜尋並關注「 冰河技術 」微信公眾號,跟冰河學習高併發程式設計技術。

最後,附上併發程式設計需要掌握的核心技能知識圖,祝大家在學習併發程式設計時,少走彎路。

相關推薦

併發優化方式竟然

寫在前面 今天,在優化程式的加鎖方式時,竟然出現了死鎖!!到底是為什麼呢?!經過仔細的分析之後,終於找到了原因。 為何需要優化加鎖方式? 在《【高併發】高併發環境下詭異的加鎖問題(你加的鎖未必安全)》一文中,我們在轉賬類TansferAccount中使用TansferAccount.class物件對程式加

併發面試官:講講併發場景下如何優化方式

## 寫在前面 > 很多時候,我們在併發程式設計中,涉及到加鎖操作時,對程式碼塊的加鎖操作真的合理嗎?還有沒有需要優化的地方呢? ## 前言 在《[【高併發】優化加鎖方式時竟然死鎖了!!](https://mp.weixin.qq.com/s?__biz=Mzg3MzE1NTIzNA==&

併發併發環境下詭異的問題(你未必安全)

宣告 特此宣告:文中有關支付寶賬戶的說明,只是用來舉例,實際支付寶賬戶要比文中描述的複雜的多。也與文中描述的完全不同。 前言 很多網友留言說:在編寫多執行緒併發程式時,我明明對共享資源加鎖了啊?為什麼還是出問題呢?問題到底出在哪裡呢?其實,我想說的是:你的加鎖姿勢正確嗎?你真的會使用鎖嗎?錯誤的加鎖方式不但

併發你知道嗎?大家都在使用Redisson實現分散式

寫在前面 忘記之前在哪個群裡有朋友在問:有出分散式鎖的文章嗎~@冰河?我的回答是:這週會有,也是【高併發】專題的。想了想,還是先發一個如何使用Redisson實現分散式鎖的文章吧?為啥?因為使用Redisson實現分散式鎖簡單啊!Redisson框架是基於Redis實現的分散式鎖,非常強大,只需要拿來使用就

併發併發分散式架構解密,不是所有的都是分散式

## 寫在前面 > 最近,很多小夥伴留言說,在學習高併發程式設計時,不太明白分散式鎖是用來解決什麼問題的,還有不少小夥伴甚至連分散式鎖是什麼都不太明白。明明在生產環境上使用了自己開發的分散式鎖,為什麼還會出現問題呢?同樣的程式,加上分散式鎖後,效能差了幾個數量級!這又是為什麼呢?今天,我們就來說說如何

併發面試官:效能優化有哪些衡量指標?需要注意什麼?

## 寫在前面 > 最近,很多小夥伴都在說,我沒做過效能優化的工作,在公司只是做些CRUD的工作,接觸不到效能優化相關的工作。現在出去找工作面試的時候,面試官總是問些很刁鑽的問題來為難我,很多我都不會啊!那怎麼辦呢?那我就專門寫一些與高併發系統相關的面試容易問到的問題吧。今天,我們就來說說在高併發場景

Java併發JUC—ReentrantReadWriteLock有坑,小心讀

好長一段時間前,某些場景需要JUC的讀寫鎖,但在某個時刻內讀寫執行緒都報超時預警(長時間無響應),看起來像是鎖競爭過程中出現死鎖(我猜)。經過排查專案並沒有能造成死鎖的可疑之處,因為業務程式碼並不複雜(僅僅是一個計算過程),經幾番折騰,把注意力轉移到JDK原始碼,正文詳細說下ReentrantReadWrit

併發壓力測試 java.io.IOException: Too many open files解決方案

 對作業系統做相關的設定,增加最大檔案控制代碼數量。Linux在Linux核心2.4.x中需要修改原始碼,然後重新編譯核心才生效。編輯Linux核心原始碼中的 include/linux/fs.h檔案,將 NR_FILE 由8192改為65536,將NR_RESERVED_FILES 由10 改為 128。編

併發併發秒殺系統架構解密,不是所有的秒殺都是秒殺

前言 很多小夥伴反饋說,高併發專題學了那麼久,但是,在真正做專案時,仍然不知道如何下手處理高併發業務場景!甚至很多小夥伴仍然停留在只是簡單的提供介面(CRUD)階段,不知道學習的併發知識如何運用到實際專案中,就更別提如何構建高併發系統了! 究竟什麼樣的系統算是高併發系統?今天,我們就一起解密高併發業務場景

併發學好併發程式設計,關鍵是要理解這三個核心問題

寫在前面 寫【高併發專題】有一段時間了,一些讀者朋友留言說,併發程式設計很難,學習了很多的知識,但是在實際工作中卻無從下手。對於一個線上產生的併發問題,又不知產生這個問題的原因究竟是什麼。對於併發程式設計,感覺上似乎是掌握了,但是真正用起來卻不是那麼回事! 其實,造成這種現象的本質原因就是沒有透徹的理解併發程

併發什麼是ForkJoin?看這一篇就夠

寫在前面 在JDK中,提供了這樣一種功能:它能夠將複雜的邏輯拆分成一個個簡單的邏輯來並行執行,待每個並行執行的邏輯執行完成後,再將各個結果進行彙總,得出最終的結果資料。有點像Hadoop中的MapReduce。 ForkJoin是由JDK1.7之後提供的多執行緒併發處理框架。ForkJoin框架的基本思想是

併發為何併發系統中都要使用訊息佇列?這次徹底懂

寫在前面 很多高併發系統中都會使用到訊息佇列中介軟體,那麼,問題來了,為什麼在高併發系統中都會使用到訊息佇列中介軟體呢?立志成為資深架構師的你思考過這個問題嗎? 本文集結了眾多技術大牛的程式設計思想,由冰河匯聚並整理而成,在此,感謝那些在技術發展道理上默默付出的前輩們! 場景分析 現在假設這樣一個場景,使

併發併發環境下該如何構建應用級快取?

寫在前面 隨著我們的系統負載越來越高,系統的效能就會有所下降,此時,我們可以很自然地想到使用快取來解決資料讀寫效能低下的問題。但是,立志成為資深架構師的你,是否能夠在高併發環境下合理並且高效的構建應用級快取呢? 快取命中率 快取命中率是從快取中讀取資料的次數與總讀取次數的比率,命中率越高越好。快取命中率=

併發不廢話,言簡意賅介紹BlockingQueue

寫在前面 最近,有不少網友留言提問:在Java的併發程式設計中,有個BlockingQueue,它是個阻塞佇列,為何要在併發程式設計裡使用BlockingQueue呢?好吧,今天,就臨時說一下BlockingQueue吧,不過今天說的不是很深入,後面咱們一起從源頭上深入剖析這個類。 BlockingQue

併發併發環境下如何防止Tomcat記憶體溢位?看完我懂

寫在前面 隨著系統併發量越來越高,Tomcat所佔用的記憶體就會越來越大,如果對Tomcat的記憶體管理不當,則可能會引發Tomcat記憶體溢位的問題,那麼,如何防止Tomcat記憶體溢位呢?我們今天就來一起探討下這個問題。 防止Tomcat記憶體溢位可以總結為兩個方案:一個是設定Tomcat啟動的初始記

併發面試官問我如何使用Nginx實現限流,我如此回答輕鬆拿到Offer

## 寫在前面 > 最近,有不少讀者說看了我的文章後,學到了很多知識,其實我本人聽到後是非常開心的,自己寫的東西能夠為大家帶來幫助,確實是一件值得高興的事情。最近,也有不少小夥伴,看了我的文章後,順利拿到了大廠Offer,也有不少小夥伴一直在刷我的文章,提升自己的內功,最終成為自己公司的核心業務開發人

併發如何設計一個支撐併發大流量的系統?這次我將設計思路分享給大家

## 寫在前面 > 最近不少小夥伴們都在問我:高併發專題我學了不少文章了,但是如何設計一個高併發的系統我還是一臉懵逼!這個問題怎麼解決呢?其實,相信不只是問我的這些小夥伴有這個困惑,就連工作(入坑)了好幾年的開發人員也都有這樣的困惑:我學習了很多的高併發課程,也看了不少的高大上的文章,可就是不知道怎麼

併發併發環境下構建快取服務需要注意哪些問題?我和阿里P9聊很久

## 寫在前面 > 週末,跟阿里的一個朋友(去年晉升為P9了)聊了很久,聊的內容幾乎全是技術,當然了,兩個技術男聊得最多的話題當然就是技術了。從基礎到架構,從演算法到AI,無所不談。中間又穿插著不少天馬行空的想象,雖然現在看起來不太實際,但是隨著技術的進步,相信五年、十年之後都會實現的。 > &

併發億級流量場景下如何為HTTP介面限流?看完我懂

## 寫在前面 > 在網際網路應用中,高併發系統會面臨一個重大的挑戰,那就是大量流高併發訪問,比如:天貓的雙十一、京東618、秒殺、搶購促銷等,這些都是典型的大流量高併發場景。關於秒殺,小夥伴們可以參見我的另一篇文章《[【高併發】高併發秒殺系統架構解密,不是所有的秒殺都是秒殺!](https://mp

併發億級流量場景下如何實現分散式限流?看完我徹底懂(文末有福利)

## 寫在前面 > 在網際網路應用中,高併發系統會面臨一個重大的挑戰,那就是大量流高併發訪問,比如:天貓的雙十一、京東618、秒殺、搶購促銷等,這些都是典型的大流量高併發場景。關於秒殺,小夥伴們可以參見我的另一篇文章《[【高併發】高併發秒殺系統架構解密,不是所有的秒殺都是秒殺!](https://mp