執行緒的活性故障

eacape發表於2022-05-14

目錄

死鎖的產生條件與規避

產生一個死鎖必須滿足以下所有條件

  • 資源互斥:資源必須是獨佔的,即這個資源只能被一個執行緒佔用
  • 資源不可被搶奪:當佔用資源的執行緒不主動釋放資源,其它執行緒無法獲取這個資源
  • 佔用並等待資源:當一個執行緒要獲取另一個資源時,如果這個資源被其它執行緒佔用,那麼它需要等待其他執行緒釋放這個資源,同時本執行緒不釋放自己佔用的資源。
  • 迴圈等待資源:各個執行緒都在請求等待其它資源且並不主動釋放自身資源,這些執行緒的請求形成了一個閉環。

    T1佔用資源A等待資源B被T2釋放同時自己不釋放資源A

    T2佔用資源B等待資源C被T3釋放同時自己不釋放資源B

    T3佔用資源C等待資源D被T4釋放同時自己不釋放資源C

    T4佔用資源D等待資源A被T1釋放同時自己不釋放資源D

想要避免死鎖只要消除以上任何一個條件即可

規避死鎖的方式

  • 鎖粗化:例如哲學家問題中,筷子是隻能被獨佔的,每個哲學家同時去拿筷子,然後哲學家們同時都拿起了自己左手的筷子,但是自己右邊的筷子卻被隔壁的哲學家握在左手,然後它們都在等待隔壁的哲學家放下左手的筷子,結果誰也放就形成了死鎖。

    上面相當於哲學家A、哲學家B、哲學家C獲取筷子A、筷子B、筷子C,相當於執行緒A、執行緒B、執行緒C獲取到了Lock-A、Lock-B、Lock-C!

    我將這個鎖"粗化"為,我們可以讓每次此只能由一個哲學家取筷子,這樣至少有一個哲學家拿到了左右兩根筷子,以至於不會形成迴圈等待資源的局面。

    實際上就是用Lock來代替Lock-A、Lock-B、Lock-C這樣同時只會有一個執行緒獲取到資源,這樣就可以有效避免死鎖的發生,但是缺點也很明顯就是降低了併發的可能性,導致資源浪費!

  • 鎖排序

    依然是哲學家問題,我們給筷子加個序號 筷子A(1) 、筷子B(2)、筷子C(3),我們在拿筷子的時候要去判斷左手旁的筷子序號小還是右手的序號小,哪個小我們先拿哪根筷子。

    可能會出現以下場景

    哲學家A先拿起筷子A(1)然後拿起了筷子C(3),然後哲學家B發現自己右手的筷子A序號更小,但是已經被哲學家A拿到了,所以他不會拿筷子B(2),然後哲學家C發現自己右手的筷子B(2)序號小於自己左手的筷子C(3),但是需要等待哲學家A放下筷子C(3)才能吃飯。

    上面哲學家的拿筷子的排列會有很多,但是由於需要按照左右手筷子的序號大小來拿起筷子(不會每個哲學家左手筷子的序號大於右手,也不會每個哲學家右手筷子的序號大於左手)所以不會出現迴圈等待的情況。

  • 使用tryLock(long, TimeUnit)來申請鎖,當一個執行緒申請一個資源的時間達到一定的界限後會放棄申請這把鎖,這樣就不會導致迴圈等待資源的場面了。

    例如哲學家問題中假如每個哲學家都拿到了右手的筷子,左手都在等待隔壁放下筷子,但是一定的時間內等不到筷子就放棄等待並且把自己的右手的筷子也放下。

    try{
      leftLock.tryLock()
    }catch(interruptedException e){
      rightLock.unLock();
      ......  
    }
    ....

死鎖的恢復

在java中使用內部鎖或者Lock.lock()方式申請的鎖並且產生了的死鎖是不可恢復的,只能通過重啟虛擬機器來去除這些死鎖。但是如果執行緒是通過Lock.lockInterruptibly()方式申請的鎖,那麼死鎖是可能被呼叫thread.interrupt()打破的。

public class DeadlockDetector extends Thread {
  static final ThreadMXBean tmb = ManagementFactory.getThreadMXBean();
  /**
   * 檢測週期(單位為毫秒)
   */
  private final long monitorInterval;

  public DeadlockDetector(long monitorInterval) {
    super("DeadLockDetector");
    setDaemon(true);
    this.monitorInterval = monitorInterval;
  }

  public DeadlockDetector() {
    this(2000);
  }

  public static ThreadInfo[] findDeadlockedThreads() {
    long[] ids = tmb.findDeadlockedThreads();
    return null == tmb.findDeadlockedThreads() ?
        new ThreadInfo[0] : tmb.getThreadInfo(ids);
  }

  public static Thread findThreadById(long threadId) {
    for (Thread thread : Thread.getAllStackTraces().keySet()) {
      if (thread.getId() == threadId) {
        return thread;
      }
    }
    return null;
  }

  public static boolean interruptThread(long threadID) {
    Thread thread = findThreadById(threadID);
    if (null != thread) {
      thread.interrupt();
      return true;
    }
    return false;
  }

  @Override
  public void run() {
    ThreadInfo[] threadInfoList;
    ThreadInfo ti;
    int i = 0;
    try {
      for (;;) {
        // 檢測系統中是否存在死鎖
        threadInfoList = DeadlockDetector.findDeadlockedThreads();
        if (threadInfoList.length > 0) {
          // 選取一個任意的死鎖執行緒
          ti = threadInfoList[i++ % threadInfoList.length];
          Debug.error("Deadlock detected,trying to recover"
                      + " by interrupting%n thread(%d,%s)%n",
                  ti.getThreadId(),
                  ti.getThreadName());
          // 給選中的死鎖執行緒傳送中斷
          DeadlockDetector.interruptThread(ti.getThreadId());
          continue;
        } else {
          Debug.info("No deadlock found!");
          i = 0;
        }
        Thread.sleep(monitorInterval);
      }// for迴圈結束
    } catch (InterruptedException e) {
      // 什麼也不做
      ;
    }
  }
}

通過呼叫Management.ThreadMXBean.findDeadlockedThreads()方法可以檢測到虛擬機器中存在死鎖的執行緒,然後對這些執行緒的死鎖進行打斷,來實現死鎖的恢復。

因為死鎖產生的原因是不可控的,因此死鎖恢復的可操作性並不強,甚至可能在死鎖的打斷過程中產生活鎖等新的問題。

訊號丟失鎖死

訊號丟失鎖死的一個典型例子是等待執行緒在執行Object.wait()/Condition.await()前沒有對保護條件進行判斷,而此時保護條件實際上可能已然成立,然而此後可能並無其他執行緒更新相應保護條件涉及的共享變數使其成立並通知等待執行緒,這就使得等待執行緒一直處於等待狀態,從而使其任務一直無法進展。

巢狀監視器死鎖

巢狀鎖可能導致執行緒始終無法通知喚醒等待執行緒的活性故障就被稱為巢狀監視器死鎖。

public class NestedMonitorDeadlockDemo {
    static Object lockA = new Object();
    static Object lockB = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                synchronized (lockA){
                    synchronized (lockB){
                        try {
                            lockB.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        };

        Thread t2 = new Thread(){
            @Override
            public void run() {
                synchronized (lockA){
                    synchronized (lockB){
                        lockB.notifyAll();
                    }
                }
            }
        };
    }
}

如上程式碼t1執行緒中lockB.wait()意味著t1執行緒釋放了lockB,但是lockA並不會被其釋放,這樣的話t2就永遠無法獲取到lockA從而就不會執行lockB.notifyAll()這樣的話t1就會永遠不會被喚醒,t2執行緒就一直會卡在lockA的獲取中。

巢狀監視器死鎖一般不會像上面程式碼中這麼"明明晃晃"的出現,而是一般會在使用一些api的時候出現,使用阻塞佇列模擬一個訊息佇列,如下圖

基於對上圖中getMsg方法和setMsg方法的原始碼深層拆解可將這兩個鎖的使用情況總結如下

==getMsg==
sychronized(NesredMonitorDeadLocalDemo.class){
  lock.lockInterruptibly();
  while(條件){
    notEmpty.await();
  }
  notFull.signal();
  lock.unlock();
}

==setMsg==
sychronized(NesredMonitorDeadLocalDemo.class){
  lock.lockInterruptibly();
  while(條件){
    notFull.await();
  }
  notEmpty.signal();
  lock.unlock();
}

經過拆解後發現,這就是一個經典的巢狀監視器死鎖,當一個執行緒執行getMsg阻塞時會釋放顯式鎖但是並不會釋放最外層的內部鎖的,另外的執行緒去訪問setMsg的時候就永遠不會獲取這個內部鎖。

執行緒飢餓

執行緒飢餓(Thread Starvation)是指執行緒一直無法獲得其所需的資源而導致其任務一直無法進展的一種活性故障。

活鎖

執行緒一直在執行狀態,但是其所執行的任務卻一直沒有進展導致,其一直擁有執行緒卻一直不釋放並且做一些沒有意義的事情。

相關文章