三分鐘秒懂死鎖產(chǎn)生原因!
一、簡介
在之前的文章中,我們介紹了synchronized同步鎖關(guān)鍵字的作用以及相關(guān)的用法,它能夠保證同一時(shí)刻最多只有一個(gè)線程執(zhí)行修飾的代碼段,以實(shí)現(xiàn)線程安全執(zhí)行的效果。
但是如果過度的使用synchronized等方式進(jìn)行加鎖,程序可能會出現(xiàn)死鎖現(xiàn)象。
那什么是死鎖呢?它有什么危害?
我們知道被synchronized修飾的代碼,當(dāng)一個(gè)線程持有一個(gè)鎖,其它線程嘗試去獲取這個(gè)鎖未獲取到時(shí),那么其它線程會進(jìn)入阻塞狀態(tài),直到線程釋放鎖才能再次擁有獲取鎖的條件。假如線程 A 持有鎖 L 并且想獲取鎖 R,線程 B 持有鎖 R 并且想獲取鎖 L,那么這兩個(gè)線程將會永久等待下去,這種情況就是最簡單的死鎖現(xiàn)象。
如果程序出現(xiàn)了死鎖,會給系統(tǒng)功能帶來非常嚴(yán)重的問題,輕則導(dǎo)致程序響應(yīng)時(shí)間變長,系統(tǒng)吞吐量變??;重則導(dǎo)致應(yīng)用中的某一個(gè)功能直接失去響應(yīng)能力無法提供服務(wù),因此我們應(yīng)該及時(shí)發(fā)現(xiàn)并避規(guī)這些問題。
當(dāng)然發(fā)生死鎖的軟件應(yīng)用,不僅限于 Java 程序,還有數(shù)據(jù)庫等,不同的是:數(shù)據(jù)庫系統(tǒng)中設(shè)計(jì)了死鎖的檢測以及從死鎖中恢復(fù)的機(jī)制,數(shù)據(jù)庫如果檢測到一組事務(wù)中發(fā)生了死鎖,將選擇一個(gè)犧牲者并放棄這個(gè)事務(wù)。
而 Java 虛擬機(jī)解決死鎖問題并沒有數(shù)據(jù)庫那么強(qiáng)大,在 Java 程序中,采用synchronized加鎖的代碼如果發(fā)生死鎖,兩個(gè)線程就不能再使用了,并且這兩個(gè)線程所在的同步代碼/代碼塊也無法再運(yùn)行了,除非殺掉服務(wù),然后重啟服務(wù)!
在實(shí)際的軟件項(xiàng)目開發(fā)過程中,死鎖其實(shí)是編程設(shè)計(jì)上的 bug,問題也比較隱晦,即使通過壓力測試也不一定能找到程序上的死鎖問題。死鎖的出現(xiàn),往往是在高負(fù)載的情況下產(chǎn)生,這種場景下比較難定位。
二、死鎖復(fù)現(xiàn)
下面我們先來看一個(gè)比較經(jīng)典的產(chǎn)生死鎖示例代碼。
public class DeadLock {
private final Object right = new Object();
private final Object left = new Object();
/**
* 加鎖順序從left -> right
*/
public void leftRight() throws Exception {
synchronized (left) {
// 模擬某個(gè)業(yè)務(wù)操作耗時(shí)
Thread.sleep(1000);
synchronized (right) {
System.out.println(Thread.currentThread().getName() + " left -> right lock.");
}
}
}
/**
* 加鎖順序right -> left
*/
public void rightLeft() throws Exception {
synchronized (right) {
// 模擬某個(gè)業(yè)務(wù)操作耗時(shí)
Thread.sleep(1000);
synchronized (left) {
System.out.println(Thread.currentThread().getName() + " right -> left lock.");
}
}
}
}
public class MyThreadA extends Thread {
private DeadLock deadLock;
public MyThreadA(DeadLock deadLock) {
this.deadLock = deadLock;
}
@Override
public void run() {
try {
deadLock.leftRight();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class MyThreadB extends Thread {
private DeadLock deadLock;
public MyThreadB(DeadLock deadLock) {
this.deadLock = deadLock;
}
@Override
public void run() {
try {
deadLock.rightLeft();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class MyThreadTest {
public static void main(String[] args) {
DeadLock deadLock = new DeadLock();
MyThreadA threadA = new MyThreadA(deadLock);
MyThreadB threadB = new MyThreadB(deadLock);
threadA.start();
threadB.start();
}
}
運(yùn)行測試類觀察控制臺,你會發(fā)現(xiàn)服務(wù)一直在運(yùn)行,什么都沒有輸出,并且無法關(guān)閉,因?yàn)槌绦蛞呀?jīng)死鎖了!
圖片
發(fā)生這個(gè)現(xiàn)象的原因,其實(shí)也很簡單。
1.線程 A 啟動之后,先獲取了left對象鎖,然后緊接著嘗試獲取right對象鎖,因?yàn)閞ight對象鎖被其它線程占有,只能進(jìn)入阻塞狀態(tài)
2.線程 B 啟動之后,先獲取了right對象鎖,然后緊接著嘗試獲取left對象鎖,因?yàn)閘eft對象鎖被其它線程占有,只能進(jìn)入阻塞狀態(tài)
3.兩個(gè)線程互相等待對方釋放鎖,程序進(jìn)入永久等待狀態(tài),因此都無法進(jìn)入打印方法體
如何定位死鎖問題呢?
我們可以通過 Java 自帶的 jps 和 jstack 工具,查看 java 進(jìn)程 id 和相關(guān)的線程堆棧信息。
定位過程如下!
2.1、通過 jps 獲得當(dāng)前 Java 虛擬機(jī)進(jìn)程的 pid
圖片
左邊的是當(dāng)前 Java 虛擬機(jī)進(jìn)程 ID,后邊是進(jìn)程名稱,其中MyThreadTest就是我們當(dāng)前運(yùn)行的測試類服務(wù)。
2.2、通過 jstack 查看進(jìn)程中的線程信息
在 jstack 后面輸入對應(yīng)的 java 進(jìn)程 ID,然后回車即可查詢到進(jìn)程中的線程情況,前面的部分,可以很清晰的看到,兩個(gè)線程都處于阻塞狀態(tài),等待獲取對應(yīng)的鎖。
圖片
因?yàn)榫€程的信息比較多,直接滑倒最底部,可以看到 JVM 給出的死鎖報(bào)告信息。
圖片
遇到這種情況,只能強(qiáng)制終止服務(wù)才能解除死鎖!
三、避免死鎖的方式
上面我們復(fù)現(xiàn)了死鎖的發(fā)生,總結(jié)下來你會發(fā)現(xiàn)死鎖的產(chǎn)生,總共有四個(gè)共同特點(diǎn):
1.互斥使用,即當(dāng)資源被一個(gè)線程占用時(shí),別的線程不能使用
2.不可搶占,資源請求者不能強(qiáng)制從資源占有者手中搶奪資源,資源只能由占有者主動釋放
3.請求和保持,當(dāng)資源請求者在請求其他資源的同時(shí)保持對原有資源的占有
4.循環(huán)等待,多個(gè)線程存在環(huán)路的鎖依賴關(guān)系而永遠(yuǎn)等待下去,例如 T1 占有 T2 的資源,T2 占有 T3 的資源,T3 占有 T1 的資源,這種情況可能會形成一個(gè)等待環(huán)路
這四個(gè)特點(diǎn)是死鎖產(chǎn)生的必要條件,只要系統(tǒng)發(fā)生死鎖,這些條件必然成立,只要能破壞其中一條即可讓死鎖消失,當(dāng)然條件一是基礎(chǔ),不能被破壞。
理解了死鎖的原因,尤其是產(chǎn)生死鎖的四個(gè)必要條件,就可以最大可能地避免產(chǎn)生死鎖和解除死鎖。
在軟件編程中,我們?nèi)绾伪苊馑梨i呢?
關(guān)于死鎖的避免,主要有以下幾種方式:
1.盡可能使用無鎖編程,使用開放調(diào)用的編碼設(shè)計(jì)
2.設(shè)計(jì)時(shí)考慮清楚鎖的順序,盡量減少嵌在的加鎖交互數(shù)量
2.盡可能的縮小鎖的范圍,防止鎖住的資源過多引發(fā)阻塞
4.使用定時(shí)鎖,比如Lock類中的tryLock方法去嘗試獲取鎖,這個(gè)方法支持在指定時(shí)間內(nèi)獲取鎖,如果等待超時(shí)會返回一個(gè)失敗信息,死鎖會自動解除。
對于死鎖的診斷,主要有以下幾種方式:
1.對代碼進(jìn)行全局分析,找出代碼中什么地方會出現(xiàn)死鎖
2.通過線程轉(zhuǎn)儲(Thread Dump)信息來分析死鎖,比如 jstack、jvisualvm、jconsole 等工具
至于死鎖的解除,主要有以下幾種方式:
1.直接強(qiáng)制終止并重啟服務(wù),如果代碼上的風(fēng)險(xiǎn)沒有消除,可能還會再次出現(xiàn)
2.采用定時(shí)鎖方案,雖然synchronized不具備這個(gè)功能,但是Lock類中的tryLock方法具備,實(shí)際編程中采用Lock中的超時(shí)機(jī)制進(jìn)行加鎖,應(yīng)用的比較多