產生死鎖的四個必要條件:
1.互斥條件:一個資源同一時刻只能被一個線程所占有。
2.持有並等待條件:一個線程T1已經持有某資源X,然后申請獲得新的資源Y,在等待過程中不釋放已有資源X。
3.不可搶占條件:其它線程不能強行搶占線程T1的資源。
4.循環等待條件:線程T1持有資源X,等待線程T2持有的資源Y,線程T2等待X。
死鎖的例子
假設線程 T1 執行賬戶 A 轉賬戶 B 的操作,賬戶 A.transfer(賬戶 B);同時線程 T2 執行賬戶 B 轉賬戶 A 的操作,賬戶 B.transfer(賬戶 A)。當 T1 和 T2 同時執行完①處的代碼時,T1 獲得了賬戶 A 的鎖(對於 T1,this 是賬戶 A),而 T2 獲得了賬戶 B 的鎖(對於 T2,this 是賬戶 B)。之后 T1 和 T2 在執行②處的代碼時,T1 試圖獲取賬戶 B 的鎖時,發現賬戶 B 已經被鎖定(被 T2 鎖定),所以 T1 開始等待;T2 則試圖獲取賬戶 A 的鎖時,發現賬戶 A 已經被鎖定(被 T1 鎖定),所以 T2 也開始等待。於是 T1 和 T2 會無期限地等待下去,也就是我們所說的死鎖了。
1 class Account { 2 private int balance; 3 // 轉賬 4 void transfer(Account target, int amt){ 5 // 鎖定轉出賬戶 6 synchronized(this){ ① 7 // 鎖定轉入賬戶 8 synchronized(target){ ② 9 if (this.balance > amt) { 10 this.balance -= amt; 11 target.balance += amt; 12 } 13 } 14 } 15 } 16 }
破壞死鎖的方法:
互斥條件:不可改變。
持有並等待條件:可以一次性給線程申請所有的資源。
不可搶占條件:線程在申請不到資源時主動釋放掉已有的資源。
循環等待條件:將資源線性排列,申請時先申請序號小的,再申請序號大的。
破壞不可搶占條件:
synchronized 申請資源的時候,如果申請不到,線程直接進入阻塞狀態了,而線程進入阻塞狀態,啥都干不了,也釋放不了線程已經占有的資源。Java 在語言層次無法解決這個問題,不過在 SDK 層面還是解決了的。java.util.concurrent 這個包下面提供的 Lock 是可以輕松解決這個問題的。
破壞循環等待條件:
假設每個賬戶都有不同的屬性 id,這個 id 可以作為排序字段,申請的時候,可以按照從小到大的順序來申請。比如下面代碼中,①~⑥處的代碼對轉出賬戶(this)和轉入賬戶(target)排序,然后按照序號從小到大的順序鎖定賬戶。這樣就不存在“循環”等待了。
1 class Account { 2 private int id; 3 private int balance; 4 // 轉賬 5 void transfer(Account target, int amt){ 6 Account left = this ① 7 Account right = target; ② 8 if (this.id > target.id) { ③ 9 left = target; ④ 10 right = this; ⑤ 11 } ⑥ 12 // 鎖定序號小的賬戶 13 synchronized(left){ 14 // 鎖定序號大的賬戶 15 synchronized(right){ 16 if (this.balance > amt){ 17 this.balance -= amt; 18 target.balance += amt; 19 } 20 } 21 } 22 } 23 }