死鎖了怎么辦?
前面說使用Account.class作為轉賬的互斥鎖,這種情況下所有的操作都串行化,性能太差,這個時候需要提升性能,肯定不能使用這種方案.
現實化轉賬問題
假設某個賬戶的所有操作都在賬本中,那轉賬操作需要兩個賬戶,這個時候有三種情況:
- 兩個賬戶的賬本都存在,這個時候一起拿走
- 兩個賬戶的賬本只存在其一,先拿一個,等待其他人把剩余一本送過來
- 兩個賬戶的賬本都沒有,等待其他人把兩個賬本都送回來
上面的邏輯其實就是使用兩把鎖實現,圖形化:
代碼實現如下:
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;
}
}
}
}
}
這個其實就是細粒度鎖. 細粒度鎖提高並行度,是性能優化的一個重要手段.,但是天下沒有免費的午餐,這種細粒度鎖可能會導致死鎖的情況發生.也就是假如現在A轉賬100給B,由張三做這個轉賬業務;B轉賬給A100元,由李四完成這個轉賬業務,這個時候張三拿到A的賬戶本,同一時刻李四拿到B的賬戶本,這個時候張三等待李四的B賬戶本,李四等待張三的A賬戶本,兩人都不會送回來,就產生的死等,死等就是變成領域的死鎖.死鎖是指一組互相競爭資源的線程因互相等待,導致永久阻塞的現象
預防死鎖
死鎖一旦產生是沒有辦法解決的,只能重啟應用. 所以解決死鎖的最好辦法就是避免死鎖,如何避免死鎖,那就要從產生死鎖的條件入手:
- 互斥,共享資源X和Y只能被一個線程占用
- 占有且等待,線程T1已經取得共享資源X,在等待共享資源Y的時候,不釋放共享資源X
- 不可搶占,其他線程不能強行搶占T1占有的資源
- 循環等待,線程T1等待線程T2占有的資源,線程T2等待線程T1占有的資源,就是循環等待
四個添加同時滿足就會產生死鎖,只要能破壞掉有一個條件,死鎖就不會產生.共享資源是沒有辦法破壞,也就是互斥是沒有辦法解決,鎖的目的就是為了互斥.
- 占有且等待: 一次性申請所有的資源就可以解決
- 不可搶占: 占用部分資源后獲取不到后續資源就釋放掉前面獲取的資源,就可以解決
- 循環等待: 按照序號申請資源來預防,也就是說給每個資源標記一個序號,沒次加鎖的時候都先獲取資源序號小的,這樣有順序就不會出現循環等待
破壞占用且等待
只需要同時申請資源就可以,同時申請這個操作是一個臨界區,需要一個Java類來管理這個臨界區,也就是定義一個角色,這個角色的兩個重要功能就是同時申請資源apply()和同時釋放資源free(),並且這個類是單例的.其實本質就是設置一個管理員,只有管理員有權限去分配資源,其他普通用戶只能去管理員那取資源,一個人操作就不會產生死鎖了.
代碼實現如下:
class Allocator {
private List<Object> als =
new ArrayList<>();
// 一次性申請所有資源
synchronized boolean apply(
Object from, Object to){
if(als.contains(from) ||
als.contains(to)){
return false;
} else {
als.add(from);
als.add(to);
}
return true;
}
// 歸還資源
synchronized void free(
Object from, Object to){
als.remove(from);
als.remove(to);
}
}
class Account {
// actr 應該為單例
private Allocator actr;
private int balance;
// 轉賬
void transfer(Account target, int amt){
// 一次性申請轉出賬戶和轉入賬戶,直到成功
while(!actr.apply(this, target));
try{
// 鎖定轉出賬戶
synchronized(this){
// 鎖定轉入賬戶
synchronized(target){
if (this.balance > amt){
this.balance -= amt;
target.balance += amt;
}
}
}
} finally {
actr.free(this, target)
}
}
}
破壞不可搶占條件
這個的核心是釋放掉已占有的資源,這個synchronized是做不到,因為synchronized申請資源的時候如果申請不到就直接進入阻塞,阻塞狀態啥也干不了.
這個時候就需要java.util.concurrent包下提供的Lock,這個等學到的時候再總結.
破壞循環等待條件
這個就需要一個id值了,保護加鎖的順序是從序號小的資源開始.
class Account {
private int id;
private int balance;
// 轉賬
void transfer(Account target, int amt){
Account left = this; ①
Account right = target; ②
// left是序號小的資源鎖
if (this.id > target.id) { ③
left = target; ④
right = this; ⑤
} ⑥
// 鎖定序號小的賬戶
synchronized(left){
// 鎖定序號大的賬戶
synchronized(right){
if (this.balance > amt){
this.balance -= amt;
target.balance += amt;
}
}
}
}
}
保證課加鎖的順序,就不會出現循環等待了.
編程世界其實是和現實世界有所關聯的,編程不就是為了解決現實生活中的問題嗎? 上面的解決死鎖的兩個方案,那個更好呢? 其實破壞循環等待條件的成本要比破壞占有且等待的成本要低,后者也鎖定了所有賬戶並且使用了死循環.相對來說,前者的成本低,但是不是絕對的,只是轉賬的這個例子中,破壞循環等待的成本比較低.