如何避免死鎖和活鎖? - simar

banq發表於2019-04-03

死鎖只能在併發(多執行緒)程式中發生,其中同步(使用鎖)執行緒訪問一個或多個共享資源(變數和物件)或指令集(臨界區)。
活鎖時當我們試圖避免死鎖時會使用非同步鎖定時發生的,其中多個執行緒對同一組鎖的競爭寫操作,為了避免獲取鎖定,允許其他執行緒第一個到達的獲得鎖,等待最終釋放鎖定後再繼續,這容易造成等待執行緒不斷重試獲取鎖造成的CPU迴圈飢餓。非同步鎖只是一種避免死鎖成為活鎖的策略。

下面是一些的理論上解決死鎖的方法,並且其中之一(第二個)是主要的原因為活鎖。

理論方法
1. 不要使用鎖
在兩個操作需要同步的情況下是不可能的,例如,簡單的銀行轉帳,您可以借記一個帳戶然後可以貸記另一個帳戶,並且在當前執行緒完成之前不允許任何其他執行緒觸及帳戶中的餘額。

2.不要阻塞鎖,如果一個執行緒無法獲取鎖,它應該釋放以前獲取的鎖,以便稍後再試
實施起來很麻煩並且可能導致飢餓(活鎖),執行緒總是重試獲取鎖。此外,這種方法可能在頻繁的執行緒上下文切換中會造成切換開銷,從而降低了系統的整體效能。

3. 讓執行緒始終以嚴格的順序請求鎖定
說起來容易做起來難。如果我們正在寫一個函式將賬戶A的資金轉移到B,我們可以寫一些類似的東西:

// at compile time, we take lock on first arg then second
public void transfer(Account A, Account B, long money) {
  synchronized (A) {
    synchronized (B) {
      A.add(amount);
      B.subtract(amount);
    }
  }
}
// at runtime we cannot track how our methods will be called
public void run() {
  new Thread(()->this.transfer(X,Y, 10000)).start();
  new Thread(()->this.transfer(Y,X, 10000)).start();
}
// this run() will create a deadlock
// first thread locks on X, waits for Y
// second thread locks on Y, waits for X


現實中的解決方案
我們可以結合鎖定順序和定時鎖定的方法來得到真正現實解決方案

1. 透過業務確定鎖的順序
我們可以透過根據帳號大小區分A和B來改進我們的方法。

// at run time, we take lock on account with smaller id first
public void transfer(Account A, Account B, long money) {
  final Account first = A.id < B.id ? A : B;
  final Account second = first == A? B: A;
  synchronized (first) {
    synchronized (second) {
      first.add(amount);
      second.subtract(amount);
    }
  }
}
// at runtime we cannot track how our methods will be called
public void run() {
  new Thread(()->this.transfer(X,Y, 10000)).start();
  new Thread(()->this.transfer(Y,X, 10000)).start();
}


如果X.id = 1111和Y.id = 2222,因為我們採取的第一個帳戶為一個較小的賬戶ID,鎖定順序執行:transfer(Y, X, 10000)和transfer(X,Y, 10000)將是一樣的。如果X的帳號小於Y,則兩個執行緒將嘗試在Y之前鎖定X,並且只有X成功後才繼續鎖定Y。

2. 業務確定tryLock / async 的時間等待的鎖請求
使用上述業務確定性鎖順序的解決方案僅適用於一個地方的邏輯轉移(...)的關聯關係,例如在我們的方法中確定如何協調資源。
我們最終可能會有其他方法/邏輯,最終使用與之不相容的排序邏輯transfer(…)。為避免在這種情況下出現死鎖,建議使用非同步鎖定,我們嘗試鎖定資源的有限/實際時間(最大事務時間)+小隨機等待時間,這樣所有執行緒都不會嘗試分別獲得太早而避免了活鎖(由於無法獲取鎖反覆嘗試而導致飢餓)

// assume AccountgetLock() gives us account's Lock  (java.util.concurrent.locks.Lock)
// Account could encapsulate lock, provide lock() /unlock()
public long getWait() { 
/// returns moving average of transfer times for last n transfers + small-random-salt in millis so all threads waiting to lock do not wake up at the same time.
//////返回最後n次傳輸的傳輸時間的移動平均值+ 小隨機時間,因此等待鎖定的所有執行緒不會同時喚醒。

}
public void transfer(Lock lockF, Lock lockS, int amount) {
  final Account first = A.id < B.id ? A : B;
  final Account second = first == A? B: A;
  final Lock lockF = first.getLock();
  final Lock lockS = second.getLock();
  boolean done = false;
  do {
    try {
      try {
        if (lockF.tryLock(getWait(), MILLISECONDS)) {
          try {
            if (lockS.tryLock(getWait(), MILLISECONDS)) {
              done = true;
            }
          } finally {
            lockS.unlock();
          }
        }
      } catch (InterruptedException e) {
        throw new RuntimeException("Cancelled");
      }
    } finally {
      lockF.unlock();
    }
  } while (!done);

}
// at runtime we cannot track how our methods will be called
public void run() {
    new Thread(()->this.transfer(X,Y, 10000)).start();
    new Thread(()->this.transfer(Y,X, 10000)).start();
}


 

相關文章