摘要:最近,在優化程式的加鎖方式時,竟然出現了死鎖!!到底是為什么呢?!經過仔細的分析之后,終于找到了原因,
本文分享自華為云社區《【高并發】優化加鎖方式時竟然死鎖了!!》,作者: 冰 河,
寫在前面
最近,在優化程式的加鎖方式時,竟然出現了死鎖!!到底是為什么呢?!經過仔細的分析之后,終于找到了原因,
為何需要優化加鎖方式?
我們在轉賬類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; } } } } }
總結
在并發編程中,使用細粒度鎖來鎖定多個資源時,要時刻注意死鎖的問題,另外,避免死鎖最簡單的方法就是阻止回圈等待條件,將系統中所有的資源設定標志位、排序,規定所有的執行緒申請資源必須以一定的順序來操作進而避免死鎖,
點擊關注,第一時間了解華為云新鮮技術~
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/540274.html
標籤:其他
上一篇:Shell 變數知多少?
