Java併發編程之鎖的活躍性問題

在安全性和活躍性之間一般存在一種制衡。當咱們使用鎖來保證線程的安全的同時,若是過分使用加鎖,可能會致使死鎖。 應用沒法從死鎖中恢復過來,因此在設計時必定要避免會排除這些可能會出現的活躍性問題。數據庫

死鎖

死鎖描述了這樣一種情景,兩個或多個線程永久阻塞,互相等待對方釋放資源編程

若是線程1鎖住了A,而後嘗試對B進行加鎖,同時線程2已經鎖住了B,接着嘗試對A進行加鎖,這時死鎖就發生了。線程1永遠得不到B,線程2也永遠得不到A,而且它們永遠也不會知道發生了這樣的事情。爲了獲得彼此的對象(A和B),它們將永遠阻塞下去。安全

pubic class LeftRightDeadlock{
    
    private Object left =new Object();
    private Object right =new Object();
    
    public void leftRight(){
        
        synchronized(left){
          synchronized(right){
              
              dosomething();
          }
        }
    }
    punlic void rightLeft(){
        synchronized(right){
            synchronized(left){
                dosomething();
            }
        }
    }
}

很常見的錯誤,若是同時2個線程去請求這2個方法就會出現死鎖數據結構

更加複雜的死鎖場景發生在數據庫事務中。一個數據庫事務可能由多條SQL更新請求組成。當在一個事務中更新一條記錄,這條記錄就會被鎖住避免其餘事務的更新請求,直到第一個事務結束。同一個事務中每個更新請求均可能會鎖住一些記錄 當多個事務同時須要對一些相同的記錄作更新操做時,就頗有可能發生死鎖併發

不過好在數據庫設計中考慮了檢測死鎖和死鎖恢復,數據庫會選擇犧牲一個事物來釋放鎖,從而讓其餘事物繼續進行。 不過Java應用並無這種處理,因此咱們在設計時要格外注意。數據庫設計

避免死鎖

加鎖順序

當多個線程須要相同的一些鎖,可是按照不一樣的順序加鎖,死鎖就很容易發生。this

若是能確保全部的線程都是按照相同的順序得到鎖,那麼死鎖就不會發生spa

按照順序加鎖是一種有效的死鎖預防機制。可是,這種方式須要你事先知道全部可能會用到的鎖,但總有些時候是沒法預知的線程

加鎖限時

另一個能夠避免死鎖的方法是在嘗試獲取鎖的時候加一個超時時間,這也就意味着在嘗試獲取鎖的過程當中若超過了這個時限該線程則放棄對該鎖請求。若一個線程沒有在給定的時限內成功得到全部須要的鎖,則會進行回退並釋放全部已經得到的鎖,而後等待一段隨機的時間再重試。這段隨機的等待時間讓其它線程有機會嘗試獲取相同的這些鎖,而且讓該應用在沒有得到鎖的時候能夠繼續運行設計

須要注意的是,因爲存在鎖的超時,因此咱們不能認爲這種場景就必定是出現了死鎖。也多是由於得到了鎖的線程(致使其它線程超時)須要很長的時間去完成它的任務

時和重試機制是爲了不在同一時間出現的競爭,可是當線程不少時,其中兩個或多個線程的超時時間同樣或者接近的可能性就會很大,所以就算出現競爭而致使超時後,因爲超時時間同樣,它們又會同時開始重試,致使新一輪的競爭,帶來了新的問題

死鎖檢測

死鎖檢測是一個更好的死鎖預防機制,它主要是針對那些不可能實現按序加鎖而且鎖超時也不可行的場景。

每當一個線程得到了鎖,會在線程和鎖相關的數據結構中將其記下。除此以外,每當有線程請求鎖,也須要記錄在這個數據結構中。

輸入圖片說明

當一個線程請求鎖失敗時,這個線程能夠遍歷鎖的關係圖看看是否有死鎖發生。例如,線程A請求鎖7,可是鎖7這個時候被線程B持有,這時線程A就能夠檢查一下線程B是否已經請求了線程A當前所持有的鎖。若是線程B確實有這樣的請求,那麼就是發生了死鎖

當出現死鎖的時候能夠給這些線程設置優先級,讓一個(或幾個)線程回退,剩下的線程就像沒發生死鎖同樣繼續保持着它們須要的鎖。若是賦予這些線程的優先級是固定不變的,同一批線程老是會擁有更高的優先級。爲避免這個問題,能夠在死鎖發生的時候設置隨機的優先級

飢餓

若是一個線程由於CPU時間所有被其餘線程搶走而得不到CPU運行時間,這種狀態被稱之爲飢餓。而該線程被飢餓致死正是由於它得不到CPU運行時間的機會。解決飢餓的方案被稱之爲公平性 – 即全部線程均能公平地得到運行機會

在Java中,下面幾個常見的緣由會致使線程飢餓:

  • 高優先級線程吞噬全部的低優先級線程的CPU時間。

你能爲每一個線程設置獨自的線程優先級,優先級越高的線程得到的CPU時間越多,線程優先級值設置在1到10之間,而這些優先級值所表示行爲的準確解釋則依賴於你的應用運行平臺。對大多數應用來講,你最好是不要改變其優先級值

  • 線程被永久堵塞在一個等待進入同步塊的狀態,由於其餘線程老是能在它以前持續地對該同步塊進行訪問。

Java的同步代碼區也是一個致使飢餓的因素。Java的同步代碼區對哪一個線程容許進入的次序沒有任何保障。這就意味着理論上存在一個試圖進入該同步區的線程處於被永久堵塞的風險,由於其餘線程老是能持續地先於它得到訪問

實現公平性

public class FairLock {
    private boolean           isLocked       = false;
    private Thread            lockingThread  = null;
    private List<QueueObject> waitingThreads =
            new ArrayList<QueueObject>();

  public void lock() throws InterruptedException{
    QueueObject queueObject           = new QueueObject();
    boolean     isLockedForThisThread = true;
    synchronized(this){
        waitingThreads.add(queueObject);
    }

    while(isLockedForThisThread){
      synchronized(this){
        isLockedForThisThread =
            isLocked || waitingThreads.get(0) != queueObject;
        if(!isLockedForThisThread){
          isLocked = true;
           waitingThreads.remove(queueObject);
           lockingThread = Thread.currentThread();
           return;
         }
      }
      try{
        queueObject.doWait();
      }catch(InterruptedException e){
        synchronized(this) { waitingThreads.remove(queueObject); }
        throw e;
      }
    }
  }

  public synchronized void unlock(){
    if(this.lockingThread != Thread.currentThread()){
      throw new IllegalMonitorStateException(
        "Calling thread has not locked this lock");
    }
    isLocked      = false;
    lockingThread = null;
    if(waitingThreads.size() > 0){
      waitingThreads.get(0).doNotify();
    }
  }
}


public class QueueObject {

    private boolean isNotified = false;

    public synchronized void doWait() throws InterruptedException {

    while(!isNotified){
        this.wait();
    }

    this.isNotified = false;

}

public synchronized void doNotify() {
    this.isNotified = true;
    this.notify();
}

public boolean equals(Object o) {
    return this == o;
}

}

FairLock新建立了一個QueueObject的實例,並對每一個調用lock()的線程進行入隊列。調用unlock()的線程將從隊列頭部獲取QueueObject,並對其調用doNotify(),以喚醒在該對象上等待的線程。經過這種方式,在同一時間僅有一個等待線程得到喚醒,而不是全部的等待線程。這也是實現FairLock公平性的核心所在

 

活鎖

活鎖是另外一種形式的活躍性問題,該問題不會阻塞線程,但也不能繼續執行,由於線程將不斷重複相同的操做,並且總會失, 這就至關於兩個在走廊相遇的人:A 向他本身的左邊靠想讓 B 過去,而B 向他的右邊靠想讓 A 過去。可見他們阻塞了對方。A 向他的右邊靠,而B向他的左邊靠,他們仍是阻塞了對方。

解決方案是,讓彼此重試的時候隨機等待一段時間,這樣就會有效避免活鎖的發生。

總結

活躍性問題是很是嚴重的問題,當應用出現活躍性故障,每每只有中斷應用程序才能解決,死鎖最爲常見,咱們要注意鎖的順序性問題,對於此類方法調用,有良好的封裝,對使用者透明也能避免鎖使用的錯誤,歡迎加入【Java併發編程交流組】:https://jq.qq.com/?_wv=1027&k=5mOvK7L

相關文章
相關標籤/搜索