【高并發】優化加鎖方式時竟然死鎖了!!
大家好,我是冰河~~
今天,在優化程序的加鎖方式時,竟然出現了死鎖!!到底是為什么呢?!經過仔細的分析之后,終于找到了原因。
為何需要優化加鎖方式?
在《【高并發】高并發環境下詭異的加鎖問題(你加的鎖未必安全)》一文中,我們在轉賬類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
此時,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; } } } } }
總結
在并發編程中,使用細粒度鎖來鎖定多個資源時,要時刻注意死鎖的問題。另外,避免死鎖最簡單的方法就是阻止循環等待條件,將系統中所有的資源設置標志位、排序,規定所有的線程申請資源必須以一定的順序來操作進而避免死鎖。
寫在最后
如果覺得文章對你有點幫助,請微信搜索并關注「 冰河技術 」微信公眾號,跟冰河學習高并發編程技術。
最后,附上并發編程需要掌握的核心技能知識圖,祝大家在學習并發編程時,少走彎路。
好了,今天就到這兒吧,我是冰河,我們下期見~~
Java JDK JVM 任務調度 多線程
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。