Java--死鎖以及死鎖的排查

最近遇到了死鎖的問題,所以這裏分析並總結下死鎖,給出一套排查解決方案。

死鎖示例一

清單一

public class SynchronizedDeadLock {

  private static final Object lockA = new Object();
  private static final Object lockB = new Object();

  /**
   * ThreadA先獲取lockA,在獲取lockB
   */
  private static class ThreadA extends java.lang.Thread {

    @Override
    public void run() {
      // 獲取臨界區A
      synchronized (lockA) {
        System.out.println("get lockA success");
        // 模擬耗時操作
        try {
          Thread.sleep(500);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        // 獲取臨界區B
        synchronized (lockB) {
          System.out.println("get lockB success");
        }
      }
    }
  }

  /**
   * ThreadB先獲取lockB,在獲取lockA
   */
  private static class ThreadB extends java.lang.Thread {

    @Override
    public void run() {
      // 獲取臨界區A
      synchronized (lockB) {
        System.out.println("get lockB success");
        // 模擬耗時操作
        try {
          Thread.sleep(500);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        // 獲取臨界區B
        synchronized (lockA) {
          System.out.println("get lockA success");
        }
      }
    }
  }
}

清單一代碼有點長,但是邏輯很簡單,有兩個臨界區變量lockAlockB,線程A先獲取到lockA在獲取lockB,線程B則與之相反順序獲取鎖,那麼就可能會有以下情況: 線程A獲取到lockA之後發現lockB已被線程B獲取,那麼此時線程A進入blocked狀態。同理線程B獲取lockA時發現其被線程A獲取,那麼線程B也進入blocked狀態,那麼這就是死鎖。

可以總結下,這種類型的死鎖源於鎖的嵌套,由於線程與線程之間的互相看對方都是亂序執行,因此加鎖的順序和釋放順序都是難以保證的,鎖的互相嵌套在多線程下是一個很危險的操作,因此需要額外注意。

死鎖示例二

清單二

public class TreeNode {
	TreeNode parent   = null;  
	List children = new ArrayList();

	public synchronized void addChild(TreeNode child){
		if(!this.children.contains(child)) {
			this.children.add(child);
			child.setParentOnly(this);
		}
	}
  
	public synchronized void addChildOnly(TreeNode child){
		if(!this.children.contains(child)){
			this.children.add(child);
		}
	}
  
	public synchronized void setParent(TreeNode parent){
		this.parent = parent;
		parent.addChildOnly(this);
	}

	public synchronized void setParentOnly(TreeNode parent){
		this.parent = parent;
	}
}

清單2的代碼來自併發編程網-死鎖,下方代碼可以理解爲一個組合模式,那麼在多線程的環境下如果線程1調用parent.addChild(child)方法的同時有另外一個線程2調用child.setParent(parent)方法,兩個線程中的parent表示的是同一個對象,child亦然,此時就會發生死鎖。下面的僞代碼說明了這個過程:

Thread 1: parent.addChild(child); //locks parent
          --> child.setParentOnly(parent);

Thread 2: child.setParent(parent); //locks child
          --> parent.addChildOnly()

也可以總結下:這種類型的死鎖本質原因也是鎖的嵌套問題,child.setParent(parent)該方法執行首先需要獲取到child這個對象鎖,然後其內部調用parent的方法則需要獲取parent的對象鎖,那麼就形成了鎖嵌套,因此會出現死鎖。

死鎖示例三

清單三是一種開發人員經常犯的錯誤,一般都是由於某些中斷操作沒有釋放掉鎖,所以也叫(Resource deadlock)比如下方的當i==5直接拋出異常,導致鎖沒有釋放,所以對於資源釋放語句一定要卸載finally中。

public void hello(int i) {
  LOCK.lock();
  System.out.println(Thread.currentThread().getName() + "--hello:"+i);
  // 異常拋出但是沒有釋放掉鎖
  if (i == 5) {
    throw new IllegalArgumentException("拋出異常,模擬獲取鎖後不釋放");
  }
  LOCK.unlock();
}

這種死鎖最可怕的地方是難以排查,使用jstack時無法分析出這一類的死鎖,你大概能得到的反饋可能線程仍然處於RUNNABLE,具體排查方法看下方的死鎖排查。

死鎖的排查

jstack or jcmd

jstackjcmd是JDK自帶的工具包,使用jstack -l pid或者jcmd pid Thread.print可以查看當前應用的進程信息,如果有死鎖也會分析出來。比如清單一中的死鎖會分析出以下結果:

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007fbea28989b8 (object 0x000000076ac710a0, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x00007fbea480a158 (object 0x000000076ac710b0, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
	at cn.mrdear.custom.lock.SynchronizedDeadLock$ThreadB.run(SynchronizedDeadLock.java:72)
	- waiting to lock <0x000000076ac710a0> (a java.lang.Object)
	- locked <0x000000076ac710b0> (a java.lang.Object)
"Thread-0":
	at cn.mrdear.custom.lock.SynchronizedDeadLock$ThreadA.run(SynchronizedDeadLock.java:48)
	- waiting to lock <0x000000076ac710b0> (a java.lang.Object)
	- locked <0x000000076ac710a0> (a java.lang.Object)

Found 1 deadlock.

在分析中明確指出發現了死鎖,是由於Thread-1Thread-0鎖的互斥導致的死鎖。

有時候文件分析不是很容易看,此時可以藉助一些工具來分析,比如http://gceasy.io/,其分析整理後使得結果更加容易看到。

資源死鎖排查

由於資源沒釋放的死鎖使用jstack等手段難以排查,這種棘手的問題一般要多次dump線程快照,參考kabutz/DeadlockLabJavaOne2012給出的經驗主要有以下兩種方式排查: 能夠控制資源死鎖的情況:

  1. 在死鎖前dump出線程快照
  2. 在死鎖後再次dump出線程快照
  3. 兩者比較

已經死鎖

  1. 每隔一段時間dump出線程快照
  2. 對比找到不會改變的那些線程再排查問題

應用自行檢查

在Java中提供了ThreadMXBean類可以幫助開發者查找死鎖,該查找效果與jstack一致,對於資源釋放不當死鎖是無法排查的。 使用方法如清單4所示,要注意的是死鎖的排查不是一個很高效的流程,要注意對應用性能的影響。 清單四

ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] threadsIds = threadMXBean.findDeadlockedThreads();

參考

http://ifeve.com/deadlock/ https://github.com/kabutz/DeadlockLabJavaOne2012

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章