Java 中的死鎖

溫酒煮Bug發表於2019-01-23

問題:你是怎麼發現死鎖並且是如何預防、如何解決的?

死鎖的定義:

這裡給出一個我對死鎖的概念的理解:

多個執行緒同時被阻塞,並且他們中的一個或多個都在等待某個被佔用資源的釋放。

再給出一種百度百科上的解釋,比較全面,便於交叉理解:

死鎖是指兩個或兩個以上的程式在執行過程中,由於競爭資源或者由於彼此通訊而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的程式稱為死鎖程式。

死鎖產生的必要條件:

  • 互斥 :一個資源每次只能被一個執行緒佔用
  • 不可剝奪:程式對正在被其他程式佔用的資源,不能強行剝奪
  • 請求與保持: 一個程式因請求資源而阻塞時,對已獲得的資源保持不放
  • 迴圈等待:若干程式之間形成一種頭尾相接的迴圈等待資源關係。

注:只要一個條件不滿足,就不會發生死鎖,所以避免死鎖,或者是解決死鎖問題,只需要破壞其中一個必要條件即可。

死鎖問題例子:

A 執行緒要在持有鎖a的前提下嘗試獲取鎖b

B 執行緒要在持有鎖b的前提下嘗試獲取鎖a

A B 在完成獲取鎖的動作之前,都不會放棄自身持有的鎖,死鎖條件達成,接下來是程式碼:

class Solution {
  public static void main(String[] args) {
    final Object a = new Object();
    final Object b = new Object();

    Thread threadA =
        new Thread(
            new Runnable() {
              @Override
              public void run() {
                synchronized (a) {
                  System.out.println("threadA in a lock");
                  try {
                    Thread.sleep(1000);
                    synchronized (b) {
                      System.out.println("threadA in b lock");
                    }
                  } catch (InterruptedException e) {
                    // ..
                  }
                }
              }
            });
    Thread threadB =
        new Thread(
            new Runnable() {
              @Override
              public void run() {
                synchronized (b) {
                  System.out.println("threadB in a lock");
                  try {
                    Thread.sleep(1000);
                    synchronized (a) {
                      System.out.println("threadB in b lock");
                    }
                  } catch (InterruptedException e) {
                    // ..
                  }
                }
              }
            });
    threadA.start();
    threadB.start();
  }
}複製程式碼

執行結果:

threadA in a lock

threadB in a lock

產生死鎖,程式明顯停滯了,沒有剩餘的輸出。

死鎖檢測:

這裡我們可以使用JDK提供的兩種檢測工具,進行簡易的死鎖檢測

Jstack 命令:

jstack工具可以用於生成Java虛擬機器當前的快照,是虛擬機器中每一條執行緒正在執行的方法堆疊的集合。

方法:

1 我們可以通過 jps獲取當前任務的程式號

E:\react\spring>jps

10256

20404 Launcher

8036 Jps

8188 Solution


2 可以確認任務的程式號是8188,然後執行jstack檢視當前程式的堆疊資訊

Found one Java-level deadlock:


"Thread-1":

waiting to lock monitor 0x17677e84 (object 0x073f0dd0, a java.lang.Object), which is held by "Thread-0"

"Thread-0": waiting to lock monitor 0x176798c4 (object 0x073f0dd8, a java.lang.Object),

which is held by "Thread-1"

譯為:

hread-1這個程式,正在等待一個鎖的釋放,這個monitor的地址是 0x17677e84,它正在被Thread-0這個執行緒持有

Thread-0這個程式,正在等待一個鎖的釋放,這個monitor的地址是 0x176798c4,它正在被Thread-1這個執行緒持有。

可以很明顯的看出,確實存在死鎖。

JConsole工具:

JConsole是JDK自帶的監控工具,在jdk/bin目錄下就可以找到,它用來連線正在執行的本地或遠端JVM,對執行在Java應用程式的資源消耗和效能進行監控,並且會以圖表的形式進行展示。並且本地佔用的伺服器記憶體很小,使用起來非常方便。

方法:

1 在命令列中敲入 jconsole 選擇程式號 8188 進行連線

Java 中的死鎖

監控圖表:

Java 中的死鎖

2 選擇執行緒選項卡,點選檢測死鎖

Java 中的死鎖

如圖鎖時,使用JConsole的時候,也會顯示類似Jstack的資訊,發現死鎖的執行緒。

如何規避死鎖:

正如之前提到的,避免死鎖,或者說解決死鎖問題,就需要破壞死鎖發生的必要條件。

1 儘量不要去使用鎖的巢狀,以確定的順序獲取鎖 ,避免可能產生的迴圈依賴問題。

修改後的程式碼:

class Solution {
  public static void main(String[] args) {
    final Object a = new Object();
    final Object b = new Object();

    Thread threadA =
        new Thread(
            new Runnable() {
              @Override
              public void run() {
                synchronized (a) {
                  System.out.println("threadA in a lock");
                }
                synchronized (b) {
                  System.out.println("threadA in b lock");
                }
              }
            });
    Thread threadB =
        new Thread(
            new Runnable() {
              @Override
              public void run() {
                synchronized (b) {
                  System.out.println("threadB in a lock");
                }
                synchronized (a) {
                  System.out.println("threadB in b lock");
                }
              }
            });
    threadA.start();
    threadB.start();
  }
}
複製程式碼

執行結果:

threadA in a lock

threadA in b lock

threadB in a lock

threadB in b lock

2 超時放棄

可以用ReentrantLock中的 tryLock(long time,TimeUnit unit)方法來方式獲取鎖,該方法會按照固定時長去等待鎖,因此執行緒可以在獲取鎖超時之後,主動釋放之前已經釋放的鎖。(內部是一個自旋鎖,不斷的嘗試獲取鎖,超時之後丟擲異常)。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

class Solution {
  public static void main(String[] args) throws InterruptedException {
    ReentrantLock lockA = new ReentrantLock();
    ReentrantLock lockB = new ReentrantLock();
    new Thread(new Runnable() {
      @Override
      public void run() {

        try {
          lockA.lock();
          System.out.println("Thread-2 in Alock");
          Thread.sleep(100);
          lockB.tryLock(1,TimeUnit.SECONDS);
          System.out.println("Thread-2 in Block");
        } catch (InterruptedException e) {
          e.printStackTrace();
        }finally{
          lockB.unlock();
          lockA.lock();
        }
      }
    }).start();
    new Thread(new Runnable() {
      @Override
      public void run() {

        try {
          lockB.lock();
          System.out.println("Thread-1 in Block");
          Thread.sleep(100);
          lockA.lock();
          System.out.println("Thread-1 in Alock");


        } catch (InterruptedException e) {
          e.printStackTrace();
        }finally{
          lockA.unlock();
          lockB.lock();
        }
      }
    }).start();
  }
}複製程式碼

執行結果:

Thread-2 in Alock

Thread-1 in Block

Thread-2 in Block

Exception in thread "Thread-0" java.lang.IllegalMonitorStateException at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151) at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261) at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457) at Solution$1.run(Solution.java:22) at java.lang.Thread.run(Thread.java:748)

雖然Thread-0 丟擲異常,但是Thread-1中的語句全部執行,並沒有發生死鎖,程式不會阻塞住。

複製程式碼


相關文章