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中的語句全部執行,並沒有發生死鎖,程式不會阻塞住。

複製程式碼

來源:https://juejin.im/post/5c4825c3e51d4552266560cf

相關文章