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
,t2
和t3
,執行的過程中,會先請求第一個物件的鎖,獲取之後,再請求第二個物件的鎖。所以當一個執行緒嘗試獲取第二個物件的鎖,而第二個物件的鎖被其他執行緒佔有的時候,第一個執行緒就會進入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 ...)
這種帶有最長超時時間的方法來指定等待的最長可以接受的時長,這樣就可以有效的避免死鎖了。