並發編程實戰(二) --- 如何避免死鎖


死鎖了怎么辦?

前面說使用Account.class作為轉賬的互斥鎖,這種情況下所有的操作都串行化,性能太差,這個時候需要提升性能,肯定不能使用這種方案.

現實化轉賬問題

假設某個賬戶的所有操作都在賬本中,那轉賬操作需要兩個賬戶,這個時候有三種情況:

  1. 兩個賬戶的賬本都存在,這個時候一起拿走
  2. 兩個賬戶的賬本只存在其一,先拿一個,等待其他人把剩余一本送過來
  3. 兩個賬戶的賬本都沒有,等待其他人把兩個賬本都送回來

上面的邏輯其實就是使用兩把鎖實現,圖形化:
transfer

代碼實現如下:

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賬戶本,兩人都不會送回來,就產生的死等,死等就是變成領域的死鎖.死鎖是指一組互相競爭資源的線程因互相等待,導致永久阻塞的現象

預防死鎖

死鎖一旦產生是沒有辦法解決的,只能重啟應用. 所以解決死鎖的最好辦法就是避免死鎖,如何避免死鎖,那就要從產生死鎖的條件入手:

  1. 互斥,共享資源X和Y只能被一個線程占用
  2. 占有且等待,線程T1已經取得共享資源X,在等待共享資源Y的時候,不釋放共享資源X
  3. 不可搶占,其他線程不能強行搶占T1占有的資源
  4. 循環等待,線程T1等待線程T2占有的資源,線程T2等待線程T1占有的資源,就是循環等待

四個添加同時滿足就會產生死鎖,只要能破壞掉有一個條件,死鎖就不會產生.共享資源是沒有辦法破壞,也就是互斥是沒有辦法解決,鎖的目的就是為了互斥.

  1. 占有且等待: 一次性申請所有的資源就可以解決
  2. 不可搶占: 占用部分資源后獲取不到后續資源就釋放掉前面獲取的資源,就可以解決
  3. 循環等待: 按照序號申請資源來預防,也就是說給每個資源標記一個序號,沒次加鎖的時候都先獲取資源序號小的,這樣有順序就不會出現循環等待
破壞占用且等待

只需要同時申請資源就可以,同時申請這個操作是一個臨界區,需要一個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;
        }
      }
    }
  } 
}

保證課加鎖的順序,就不會出現循環等待了.

編程世界其實是和現實世界有所關聯的,編程不就是為了解決現實生活中的問題嗎? 上面的解決死鎖的兩個方案,那個更好呢? 其實破壞循環等待條件的成本要比破壞占有且等待的成本要低,后者也鎖定了所有賬戶並且使用了死循環.相對來說,前者的成本低,但是不是絕對的,只是轉賬的這個例子中,破壞循環等待的成本比較低.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM