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可以喚醒物件的指定休眠執行緒。