Java線程和多線程(九)——死鎖

Java中的死鎖指的就是一種多於兩個線程永遠阻塞的特殊狀況。Java中的死鎖狀態至少需要多於兩個線程以及資源的時候纔會產生。這裏,我寫了一個產生死鎖的程序,並且講下如何分析死鎖。

首先來看一下產生死鎖的程序:

package com.sapphire.threads;

public class ThreadDeadlock {

    public static void main(String[] args) throws InterruptedException {
        Object obj1 = new Object();
        Object obj2 = new Object();
        Object obj3 = new Object();

        Thread t1 = new Thread(new SyncThread(obj1, obj2), "t1");
        Thread t2 = new Thread(new SyncThread(obj2, obj3), "t2");
        Thread t3 = new Thread(new SyncThread(obj3, obj1), "t3");

        t1.start();
        Thread.sleep(5000);
        t2.start();
        Thread.sleep(5000);
        t3.start();

    }
}

class SyncThread implements Runnable{
    private Object obj1;
    private Object obj2;

    public SyncThread(Object o1, Object o2){
        this.obj1=o1;
        this.obj2=o2;
    }
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(name + " acquiring lock on "+obj1);
        synchronized (obj1) {
            System.out.println(name + " acquired lock on "+obj1);
            work();
            System.out.println(name + " acquiring lock on "+obj2);
            synchronized (obj2) {
                System.out.println(name + " acquired lock on "+obj2);
                work();
            }
            System.out.println(name + " released lock on "+obj2);
        }
        System.out.println(name + " released lock on "+obj1);
        System.out.println(name + " finished execution.");
    }

    private void work() {
        try {
            Thread.sleep(30000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在上面的程序中,我們看到SyncThread是通過實現了Runnable接口來實現的多線程的,它內部包含兩個Object對象,通過synchronized代碼塊 來獲取對象鎖。

在主方法中,我定義了3個線程,分別是t1,t2t3,運行的過程中,會先請求第一個對象的鎖,獲取之後,再請求第二個對象的鎖。所以當一個線程嘗試獲取第二個對象的鎖,而第二個對象的鎖被其他線程佔有的時候,第一個線程就會進入wait狀態,而第二個線程所需要的資源也在由第三個線程所鎖定,所以三個線程構成的循環構成了死鎖。

如果我執行了上面的程序,會有如下輸出,但是程序不會結束,因爲線程死鎖而導致的線程無法結束。

t1 acquiring lock on java.lang.Object@fdfdda6
t1 acquired lock on java.lang.Object@fdfdda6
t2 acquiring lock on java.lang.Object@51dca821
t2 acquired lock on java.lang.Object@51dca821
t3 acquiring lock on java.lang.Object@25c8063f
t3 acquired lock on java.lang.Object@25c8063f
t1 acquiring lock on java.lang.Object@51dca821
t2 acquiring lock on java.lang.Object@25c8063f
t3 acquiring lock on java.lang.Object@fdfdda6

從上面的輸出之中,我們可以清晰的鑑定出線程是否處於死鎖狀態,但是在實際的應用狀態下是很難獲得這些輸出來方便開發者debug的。

如何檢測死鎖

想要檢測到Java中的死鎖,我們需要看到應用的Thread Dump的信息。在前文
之中,我們知道如何獲取應用的Thread Dump信息。通過jcmd命令,如下信息是上面程序的Thread Dump的信息:

26784:
2016-10-13 18:15:19
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.0-b70 mixed mode):

"DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x00000000026ee800 nid=0x3f84 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"t3" #12 prio=5 os_prio=0 tid=0x000000001adf4000 nid=0x2414 waiting for monitor entry [0x000000001bc8f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at net.ethanpark.common.thread.SyncThread.run(ThreadDeadLock.java:44)
        - waiting to lock <0x00000007811d9750> (a java.lang.Object)
        - locked <0x00000007811d9770> (a java.lang.Object)
        at java.lang.Thread.run(Unknown Source)

"t2" #11 prio=5 os_prio=0 tid=0x000000001adf3800 nid=0x1ef0 waiting for monitor entry [0x000000001bf9f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at net.ethanpark.common.thread.SyncThread.run(ThreadDeadLock.java:44)
        - waiting to lock <0x00000007811d9770> (a java.lang.Object)
        - locked <0x00000007811d9760> (a java.lang.Object)
        at java.lang.Thread.run(Unknown Source)

"t1" #10 prio=5 os_prio=0 tid=0x000000001aded000 nid=0x4b3c waiting for monitor entry [0x000000001bdff000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at net.ethanpark.common.thread.SyncThread.run(ThreadDeadLock.java:44)
        - waiting to lock <0x00000007811d9760> (a java.lang.Object)
        - locked <0x00000007811d9750> (a java.lang.Object)
        at java.lang.Thread.run(Unknown Source)

"Service Thread" #9 daemon prio=9 os_prio=0 tid=0x000000001adbc800 nid=0x4be8 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread2" #8 daemon prio=9 os_prio=2 tid=0x000000001ad4e800 nid=0x8124 waiting on condition [0x00000000000000
00]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #7 daemon prio=9 os_prio=2 tid=0x000000001ad4d800 nid=0x5370 waiting on condition [0x00000000000000
00]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #6 daemon prio=9 os_prio=2 tid=0x0000000019b1b800 nid=0x64a0 waiting on condition [0x00000000000000
00]
   java.lang.Thread.State: RUNNABLE

"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000000001ad4b000 nid=0x3b24 waiting on condition [0x0000000000000000]

   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000000001ad4a000 nid=0x56d0 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x0000000019ab2000 nid=0x58e4 in Object.wait() [0x000000001ad2f000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x0000000781226bd0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(Unknown Source)
        - locked <0x0000000781226bd0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(Unknown Source)
        at java.lang.ref.Finalizer$FinalizerThread.run(Unknown Source)

"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000000019aa8800 nid=0x26c8 in Object.wait() [0x000000001ab0f000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x0000000781208210> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Unknown Source)
        at java.lang.ref.Reference$ReferenceHandler.run(Unknown Source)
        - locked <0x0000000781208210> (a java.lang.ref.Reference$Lock)

"VM Thread" os_prio=2 tid=0x0000000019aa4800 nid=0x4880 runnable

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00000000025bc000 nid=0x57f8 runnable

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00000000025bd800 nid=0x6bb8 runnable

"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00000000025bf000 nid=0x3a4 runnable

"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00000000025c0800 nid=0x7b90 runnable

"VM Periodic Task Thread" os_prio=2 tid=0x000000001adc9000 nid=0x6db8 waiting on condition

JNI global references: 7


Found one Java-level deadlock:
=============================
"t3":
  waiting to lock monitor 0x0000000019aafcf8 (object 0x00000007811d9750, a java.lang.Object),
  which is held by "t1"
"t1":
  waiting to lock monitor 0x0000000019aad0f8 (object 0x00000007811d9760, a java.lang.Object),
  which is held by "t2"
"t2":
  waiting to lock monitor 0x0000000019aafc48 (object 0x00000007811d9770, a java.lang.Object),
  which is held by "t3"

Java stack information for the threads listed above:
===================================================
"t3":
        at net.ethanpark.common.thread.SyncThread.run(ThreadDeadLock.java:44)
        - waiting to lock <0x00000007811d9750> (a java.lang.Object)
        - locked <0x00000007811d9770> (a java.lang.Object)
        at java.lang.Thread.run(Unknown Source)
"t1":
        at net.ethanpark.common.thread.SyncThread.run(ThreadDeadLock.java:44)
        - waiting to lock <0x00000007811d9760> (a java.lang.Object)
        - locked <0x00000007811d9750> (a java.lang.Object)
        at java.lang.Thread.run(Unknown Source)
"t2":
        at net.ethanpark.common.thread.SyncThread.run(ThreadDeadLock.java:44)
        - waiting to lock <0x00000007811d9770> (a java.lang.Object)
        - locked <0x00000007811d9760> (a java.lang.Object)
        at java.lang.Thread.run(Unknown Source)

Found 1 deadlock.

可以看到,Thread Dump的輸出清晰的告訴我們存在死鎖,還有引起死鎖狀態的線程和相關資源。

想要分析死鎖,我們需要查看處於阻塞狀態的線程,還有等待鎖定的資源。每個資源都有自己特有的ID,我們可以通過Dump信息看到線程所鎖定的對象和請求的對象。如上面的輸出可以看出,t3線程等待獲取0x00000007811d9750的對象鎖,已經鎖定了0x00000007811d9770對象,t3線程期望獲取的對象鎖正由t1線程所鎖定。

一旦我們通過Thread Dump分析出了死鎖,以及引起死鎖的線程,我們就需要修改代碼來避免死鎖。

如何避免死鎖

關於避免死鎖,有如下一些方式可以避免絕大多數的死鎖

避免嵌套鎖

這是產生死鎖的最常見的一種情況了。如果已經獲得了一個鎖定的資源,請避免在鎖定另一個。如果僅僅開發者僅僅使用一個對象鎖的話,是很難產生死鎖的。比如說:下面的代碼就是上面代碼的另一個實現方案,就不會產生死鎖:

    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(name + " acquiring lock on " + obj1);
        synchronized (obj1) {
            System.out.println(name + " acquired lock on " + obj1);
            work();
        }
        System.out.println(name + " released lock on " + obj1);
        System.out.println(name + " acquiring lock on " + obj2);
        synchronized (obj2) {
            System.out.println(name + " acquired lock on " + obj2);
            work();
        }
        System.out.println(name + " released lock on " + obj2);

        System.out.println(name + " finished execution.");
    }

僅僅在需要的情況下進行資源鎖定

開發者可以獲取指定資源的鎖,但是僅僅只獲取一個資源的鎖。仍然就上面的例子來講。上面的程序運行已經獲取了一個對象資源,但是在我們鎖定了整個對象,如果我們只是針對其中一個實例域的話,完全可以只同步其中的一個實例域,而不要針對整個對象上鎖。

避免無限制的等待

如果兩個線程都通過Thread.join()無限制的等待另一個線程結束的話,那麼是很有可能產生死鎖的。開發者完全可以通過調用Thread.join(long ...)這種帶有最長超時時間的方法來指定等待的最長可以接受的時長,這樣就可以有效的避免死鎖了。

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