上一篇文章共享資源那麼多,如何用一把鎖保護多個資源? 文章咱們談到了銀行轉帳經典案例,其中有兩個問題:html
如何解決這兩個問題呢?我們先換好衣服穿越回到過去尋找一下錢莊,一塊兒透過現象看本質,dengdeng deng.......java
來到錢莊,告訴櫃員你要給鐵蛋兒轉 100 銅錢,這時櫃員轉身在牆上尋找你和鐵蛋兒的帳本,此時櫃員可能面臨三種狀況:git
放慢櫃員的取帳本操做,他必定是先拿到你的帳本,而後再去拿鐵蛋兒的帳本,兩個帳本都拿到(理想狀態)以後才能完成轉帳,用程序模型來描述一下這個拿取帳本的過程:github
咱們繼續用程序代碼描述一下上面這個模型:面試
class Account { private int balance; // 轉帳 void transfer(Account target, int amt){ // 鎖定轉出帳戶 synchronized(this) { // 鎖定轉入帳戶 synchronized(target) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } } }
這個解決方案看起來很完美,解決了文章開頭說的兩個問題,但真是這樣嗎?redis
咱們剛剛說過的理想狀態是錢莊只有一個櫃員(既單線程)。隨着錢莊規模變大,牆上早已掛了很是多個帳本,錢莊爲了應對繁忙的業務,開通了多個窗口,此時有多個櫃員(多線程)處理錢莊業務。數據庫
櫃員 1 正在辦理給鐵蛋兒轉帳的業務,但只拿到了你的帳本;櫃員 2 正在辦理鐵蛋兒給你轉帳的業務,但只拿到了鐵蛋兒的帳本,此時雙方出現了尷尬狀態,兩位櫃員都在等待對方歸還帳本爲當前客戶辦理轉帳業務。編程
現實中櫃員會溝通,喊出一嗓子 老鐵,鐵蛋兒的帳本先給我用一下,用完還給你,但程序卻沒這麼智能,synchronized 內置鎖很是執着,它會告訴你「死等」的道理,最終出現死鎖多線程
Java 有了 synchronized 內置鎖,還發明瞭顯示鎖 Lock,是否是就爲了治一治 synchronized 「死等」的執着呢?😏併發
如何解決上面的問題呢?正所謂知己知彼方能百戰不殆,咱們要先了解什麼狀況會發生死鎖,才能知道如何避免死鎖,很幸運咱們能夠站在巨人的肩膀上看待問題
Coffman
總結出了四個條件說明能夠發生死鎖的情形:
**互斥條件:**指進程對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個進程佔用。若是此時還有其它進程請求資源,則請求者只能等待,直至佔有資源的進程用畢釋放。
**請求和保持條件:**指進程已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它進程佔有,此時請求進程阻塞,但又對本身已得到的其它資源保持不放。
**不可剝奪條件:**指進程已得到的資源,在未使用完以前,不能被剝奪,只能在使用完時由本身釋放。
**環路等待條件:**指在發生死鎖時,必然存在一個進程——資源的環形鏈,即進程集合{P1,P2,···,Pn}中的 P1 正在等待一個 P2 佔用的資源;P2 正在等待 P3 佔用的資源,……,Pn 正在等待已被 P0 佔用的資源。
這幾個條件很好理解,其中「互斥條件」是併發編程的根基,這個條件沒辦法改變。但其餘三個條件都有改變的可能,也就是說破壞另外三個條件就不會出現上面說到的死鎖問題
每一個櫃員均可以取放帳本,很容易出現互相等待的狀況。要想破壞請求和保持條件,就要一次性拿到全部資源。
做爲程序猿你必定聽過這句話:
任何軟件工程遇到的問題均可以經過增長一箇中間層來解決
咱們不容許櫃員均可以取放帳本,帳本要由單獨的帳本管理員來管理
也就是說帳本管理員拿取帳本是臨界區,若是隻拿到其中之一的帳本,那麼不會給櫃員,而是等待櫃員下一次詢問是否兩個帳本都在
//帳本管理員 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){ // 一次性申請轉出帳戶和轉入帳戶,直到成功 while(!accountBookManager.getAllRequiredAccountBook(this, target)){ return; } try{ // 鎖定轉出帳戶 synchronized(this){ // 鎖定轉入帳戶 synchronized(target){ if (this.balance > amt){ this.balance -= amt; target.balance += amt; } } } } finally { accountBookManager.releaseObtainedAccountBook(this, target); } } }
上面已經給了你小小的提示,爲了解決內置鎖的執着,Java 顯示鎖支持通知(notify/notifyall)和等待(wait),也就是說該功能能夠實現喊一嗓子 老鐵,鐵蛋兒的帳本先給我用一下,用完還給你 的功能,這個後續將到 Java SDK 相關內容時會作說明
破壞環路等待條件也很簡單,咱們只須要將資源序號大小排序獲取就會解決這個問題,將環路拆除
繼續用代碼來講明:
class Account { private int id; private int balance; // 轉帳 void transfer(Account target, int amt){ Account smaller = this Account larger = target; // 排序 if (this.id > target.id) { smaller = target; larger = this; } // 鎖定序號小的帳戶 synchronized(smaller){ // 鎖定序號大的帳戶 synchronized(larger){ if (this.balance > amt){ this.balance -= amt; target.balance += amt; } } } } }
當 smaller 被佔用時,其餘線程就會被阻塞,也就不會存在死鎖了.
在實際業務中,關於 Account 都會是數據庫對象,咱們能夠經過事務或數據庫的樂觀鎖來解決的。另外分佈式系統中,帳本管理員這個角色的處理也可能會用 redis 分佈式鎖來解決.
在處理破壞請求和保持條件時,咱們使用的是 while 循環方式來不斷請求鎖的時候,在實際業務中,咱們會有 timeout 的設置,防止無休止的浪費 CPU 使用率
另外你們能夠嘗試使用阿里開源工具 Arthas 來查看 CPU 使用率,線程等相關問題,github 上有明確的說明
計算機的計算能力遠遠超過人類,可是他的智慧還須要有帶提升,當看待併發問題時,咱們每每認爲人類的最基本溝通計算機也能夠作到,其實否則,仍是那句話,編寫併發程序,要站在計算機的角度來看待問題
粗粒度鎖咱們不提倡,因此會使用細粒度鎖,但使用細粒度鎖的時候,咱們要嚴格按照 Coffman 的四大條件來逐條判斷,這樣再應用咱們這幾個解決方案來解決就行了
public void transfer(Account target, int amt){ // 一次性申請轉出帳戶和轉入帳戶,直到成功 while(accountBookManager.getAllRequiredAccountBook(this, target)){} try{ // 鎖定轉出帳戶 synchronized(this){ // 鎖定轉入帳戶 synchronized(target){ if (this.balance > amt){ this.balance -= amt; target.balance += amt; } } } } finally { accountBookManager.releaseObtainedAccountBook(this, target); } } }
歡迎關注個人公衆號 「日拱一兵」,趣味原創解析Java技術棧問題,將複雜問題簡單化,將抽象問題圖形化落地 若是對個人專題內容感興趣,或搶先看更多內容,歡迎訪問個人博客 dayarch.top