問題:你是怎麼發現死鎖並且是如何預防、如何解決的?
死鎖的定義:
這裡給出一個我對死鎖的概念的理解:
多個執行緒同時被阻塞,並且他們中的一個或多個都在等待某個被佔用資源的釋放。
再給出一種百度百科上的解釋,比較全面,便於交叉理解:
死鎖是指兩個或兩個以上的程式在執行過程中,由於競爭資源或者由於彼此通訊而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的程式稱為死鎖程式。
死鎖產生的必要條件:
- 互斥 :一個資源每次只能被一個執行緒佔用
- 不可剝奪:程式對正在被其他程式佔用的資源,不能強行剝奪
- 請求與保持: 一個程式因請求資源而阻塞時,對已獲得的資源保持不放
- 迴圈等待:若干程式之間形成一種頭尾相接的迴圈等待資源關係。
注:只要一個條件不滿足,就不會發生死鎖,所以避免死鎖,或者是解決死鎖問題,只需要破壞其中一個必要條件即可。
死鎖問題例子:
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 進行連線
監控圖表:
2 選擇執行緒選項卡,點選檢測死鎖
如圖鎖時,使用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中的語句全部執行,並沒有發生死鎖,程式不會阻塞住。
複製程式碼