1. 程式人生 > 其它 >Java開發之多執行緒死鎖問題排查與解決

Java開發之多執行緒死鎖問題排查與解決

死鎖問題

死鎖定義

多執行緒程式設計中,因為搶佔資源造成了執行緒無限等待的情況,此情況稱為死鎖。

死鎖舉例

注意:執行緒和鎖的關係是:一個執行緒可以擁有多把鎖,一個鎖只能被一個執行緒擁有。

當兩個執行緒分別擁有一把​java培訓​各自的鎖之後,又嘗試去獲取對方的鎖,這樣就會導致死鎖情況的發生,具體先看下面程式碼:

/**
* 執行緒死鎖問題
*/
public class DeadLock {
public static void main(String[] args) {
//建立兩個鎖物件
Object lock1 = new Object();
Object lock2 = new Object();

//建立子執行緒
/*
執行緒1:①先獲得鎖1 ②休眠1s,讓執行緒2獲得鎖2 ③執行緒1嘗試獲取鎖2 執行緒2同理
*/
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
//執行緒1業務邏輯
synchronized(lock1){
System.out.println("執行緒1得到了鎖子1");
try {
//休眠1s,讓執行緒2先得到鎖2
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行緒1嘗試獲取鎖2...");
synchronized(lock2){
System.out.println("執行緒1獲得了鎖2!");
}
}
}
},"執行緒1");


Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
//執行緒2業務邏輯
synchronized(lock2){
System.out.println("執行緒2得到了鎖子2");
try {
//休眠1s,讓執行緒1先得到鎖1;因為執行緒是併發執行我們不知道誰先執行
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行緒2嘗試獲取鎖1...");
synchronized(lock1){
System.out.println("執行緒2獲得了鎖1");
}
}
}
},"執行緒2");
thread1.start();
thread2.start();
}
}

程式執行結果如下:

可以看出,執行緒1嘗試獲取了鎖2,執行緒2嘗試獲取了鎖1,但是二者並沒有獲取到對方的鎖;這就發生了所謂的“死鎖”!

如何排查死鎖

想要排查死鎖具體細節,可以通過三個工具(位於jdk安裝路徑bin目錄)去排查,現在就給大家介紹一下:

1.jconsole

可以看出,執行緒1和執行緒2發生了死鎖,死鎖發生的位置一目瞭然

2.jvisualvm

可以看出,發生了死鎖,執行緒1和執行緒2嘗試獲取的鎖是對方的鎖。

3.jmc

可以看出,同樣檢測出了死鎖情況

無論是用哪個工具排查死鎖情況都是OK的。

死鎖發生的條件

1.互斥條件(一個鎖只能被一個執行緒佔有,當一個鎖被一個執行緒持有之後,不能再被其他執行緒持有);

2.請求擁有(一個執行緒擁有一把鎖之後,又去嘗試請求擁有另外一把鎖);可以解決

3.不可剝奪(一個鎖被一個執行緒佔有之後,如果該執行緒沒有釋放鎖,其他執行緒不能強制獲得該鎖);

4.環路等待條件(多執行緒獲取鎖時形成了一個環形鏈)可以解決

怎麼解決死鎖問題?

環路等待條件相對於請求擁有更容易實現,那麼通過破壞環路等待條件解決死鎖問題

破壞環路等待條件示意圖:

針對於上面死鎖舉例中程式碼,解決死鎖,具體看下面程式碼:

public class SolveDeadLock {
public static void main(String[] args) {
//建立兩個鎖物件
Object lock1 = new Object();
Object lock2 = new Object();

Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
//執行緒1業務邏輯
synchronized(lock1){
System.out.println("執行緒1得到了鎖子1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行緒1嘗試獲取鎖2...");
synchronized(lock2){
System.out.println("執行緒1獲得了鎖2!");
}
}
}
},"執行緒1");

Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
//執行緒2業務邏輯
synchronized(lock1){
System.out.println("執行緒2得到了鎖子1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行緒2嘗試獲取鎖2...");
synchronized(lock2){
System.out.println("執行緒2獲得了鎖2");
}
}
}
},"執行緒2");
thread1.start();
thread2.start();
}
}

程式執行結果如下:

可以看出,通過破壞環路等待條件完美解決了死鎖問題

執行緒通訊機制(wait/notify/notifyAll)

定義

執行緒通訊機制:一個執行緒的動作可以讓另外一個執行緒感知到,這就是執行緒通訊機制。

wait():讓當前執行緒進入休眠等待狀態;

notify():喚醒當前物件上的休眠等待執行緒;

notifyAll():喚醒當前物件上的所有休眠等待執行緒。

相關面試重點

面試問題:

1.wait()使用時為什麼需要加鎖?

因為wait()必須在同步方法或者同步塊中使用,也就是說wait()需要配合加鎖一起使用(比如synchronized或Lock),呼叫物件呼叫wait()如果沒有適當的鎖,就會引發異常,因此說wait()使用時需要加鎖。

2.wait()使用為什麼要釋放鎖?

wait()是Objetc類中一個例項方法,預設是不傳任何值的,不傳值的時候表示讓當前執行緒處於永久休眠等待狀態,這樣會造成一個鎖被一個執行緒長時間一直擁有,為了避免這種問題的發生,使用wait()後必須釋放鎖。

wait()/notify()/notifyAll()使用時注意事項:

使用這三個方法時都必須進行加鎖;

2.加鎖的物件和呼叫wait()/notify()/notifyAll()物件必須是同一個物件;

3.一組wait()/notify()/notifyAll()必須是同一個物件;

4.notify()只能喚醒當前物件上的一個休眠等到執行緒;而notifyAll()可以喚醒當前物件上的所有休眠等待執行緒。

sleep(0)和wait(0)的區別:

1.sleep()是Thread類中一個靜態方法,wait()是Object類中一個普通的成員方法;

2.sleep(0)會立即觸發一次CPU的搶佔執行,wait(0)會讓當前執行緒無限休眠等待下去。

wait()和sleep()的區別:

相同點:

1.都會讓當前執行緒進行休眠等待;

2.使用二者時都需處理InterruptedException異常(try/catch)。

不同點:

1.wait()是Object中普通成員方法,sleep是Thread中靜態方法;

2.wait()使用可以不穿引數,sleep()必須傳入一個大於等於0的引數;

3.wait()使用時必須配合加鎖一起使用,sleep()使用時不需要加鎖;

4.wait()使用時需要釋放鎖,如果sleep()加鎖後不會釋放鎖;

5.wait()會讓當前執行緒進入WAITING狀態(預設沒有明確的等待時間,當被別的執行緒喚醒或者wait()傳參後超過等待時間量自己喚醒,將進入就緒狀態),sleep()會讓當前執行緒進入TIMED_WAITING狀態(有明確的結束等待時間,但是這是死等的方式,休眠結束後進入就緒狀態)。

*為什麼wait()處於Object中而不是Thread中?(有點繞 我有點懵了…)

wait()的呼叫必須進行加鎖和釋放鎖操作,而鎖是屬於物件級別非執行緒級別,也就是說鎖針對於物件進行操作而不是執行緒;而執行緒和鎖是一對多的關係,一個執行緒可以擁有多把鎖,而一個執行緒只能被一個執行緒擁有,為了靈活操作,就將wait()放在Object中。

LockSupport

LockSupport是對wait()的升級,無需加鎖也無需釋放鎖;

LockSupport.park()讓執行緒休眠,和wait()一樣會讓執行緒進入WAITING狀態;

LockSupport.unpark()喚醒執行緒,可以喚醒物件上指定的休眠等待執行緒;(優勢)

LockSupport與wait()區別

wait()與LockSupport的區別:

相同點:

1.二者都可以讓執行緒進入休眠等待狀態;

2.二者都可以傳參或者不傳參,讓執行緒都會進入到WAITING狀態。

不同點:

1.wait()需要配合加鎖一起使用,LockSupport無需加鎖;

2.wait()只能喚醒物件的隨機休眠執行緒和全部執行緒,LockSupport可以喚醒物件的指定休眠執行緒。