在并發(fā)環(huán)境中,我們?yōu)榱吮WC共享可變數(shù)據(jù)的線程安全性,需要使用加鎖機制,如果鎖使用不當可能會引起死鎖,線程饑餓等問題。
在JAVA應(yīng)用程序中如果發(fā)生死鎖,程序是無法自動恢復的,嚴重會造成程序崩潰,所以開發(fā)中在設(shè)計階段就要規(guī)避死鎖發(fā)生的情況。
什么是死鎖
死鎖:每個線程擁有其他線程需要的資源,同時又等待其他線程擁有的資源,并且每個線程在獲得所需要的資源前都不會放棄已經(jīng)擁有的資源。
程序死鎖發(fā)生的場景:
1)交叉鎖導致死鎖
在線程A持有鎖L并想獲取鎖R的同時,線程B持有鎖R并嘗試獲得鎖L,那么這兩個線程將永遠的阻塞下去。交叉鎖的發(fā)生一般是因為線程以不同的順序獲取鎖。
2)資源死鎖
內(nèi)存不足或者我們在程序中使用了線程池和信號量對資源進行限制時,兩個線程互相等待彼此釋放資源而進入永久阻塞。
3)死循環(huán)死鎖
程序由于代碼缺陷或者重試機制而使代碼陷入死循環(huán),造成了內(nèi)存和cpu的大量消耗而使線程進入阻塞。
當Java程序發(fā)生死鎖時,阻塞的線程將永遠不能使用了,而且可能造成程序停止或者使CPU飆高使程序性能很差。恢復程序的唯一方式就是重啟應(yīng)用。
死鎖的發(fā)生大多數(shù)是偶然情況,并不代表一個類發(fā)生死鎖,它就一直死鎖,這也是死鎖難以排查的原因。
通過死鎖發(fā)生的場景我們可以總結(jié)出死鎖發(fā)生的條件:
- 互斥:即鎖具有排他性,只有一個線程能夠獲取鎖;
- 占有且等待:線程獲取到鎖時,如果需要的資源沒有獲取到將一直阻塞等待需要的資源;
- 不可搶占:獲取鎖的線程持有的資源不能被其他線程搶占;
- 循環(huán)等待:陷入死鎖等待的線程一定是形成了一個循環(huán)等待環(huán)路。
死鎖的檢測
如果一個程序一次最多獲得一個鎖,那么就不會發(fā)生死鎖問題,但是開發(fā)中經(jīng)常出現(xiàn)程序需要獲取多個鎖的場景,那么這個時候就必須考慮鎖的順序問題。
如果所有的線程以固定的順序獲取鎖也是不會出現(xiàn)死鎖問題的,當線程試圖以不同的順序來獲取鎖時,死鎖將會發(fā)生。
下面的示例將會發(fā)生死鎖:
public class DeadlockTest {
//創(chuàng)建兩個鎖對象
private final Object leftMonitor = new Object();
private final Object rightMonitor = new Object();
/**
* 持有L鎖想要獲取R鎖
*/
@SneakyThrows
public void leftForRight() {
synchronized (leftMonitor){
//休眠一下,給R加鎖的機會
TimeUnit.SECONDS.sleep(1);
synchronized (rightMonitor){
System.out.println("leftForRight獲取到鎖");
}
}
}
/**
* 持有R鎖獲取L鎖
*/
public void rightForLeft() {
synchronized (rightMonitor){
synchronized (leftMonitor){
System.out.println("rightForLeft獲取到鎖");
}
}
}
public static void main(String[] args) {
DeadlockTest deadlockTest = new DeadlockTest();
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(()->{
deadlockTest.leftForRight();
});
executor.execute(()->{
deadlockTest.rightForLeft();
});
executor.shutdown();
}
}
我們可以通過JDK提供的jstack或者jconsole工具查看死鎖信息。
jstack -l pid查看堆棧信息:
或者jconsole連接到進程上:
通過堆棧信息能夠很直接看到死鎖信息。
linux環(huán)境下dump出堆棧信息的方法我們后續(xù)再聊。
死鎖的避免
我們可以通過打破死鎖發(fā)生的條件來避免死鎖。
程序中的業(yè)務(wù)要求我們必須使用獨占鎖而不能使用共享鎖,那我們就不能打破鎖的互斥性。
破壞占有且等待:一次性申請所有資源;
破壞不可搶占:使用顯示鎖Lock中的tryLock功能來代替內(nèi)置鎖synchronized,可以檢測死鎖和從死鎖中恢復過來。使用內(nèi)置鎖的線程獲取不到鎖會被阻塞,而顯式鎖可以指定一個超時時限(Timeout),在等待設(shè)置的時間后tryLock就會返回一個失敗信息,也會釋放其擁有的資源。
破壞循環(huán)等待:使線程按照固定的順序獲取鎖,在設(shè)計中我們應(yīng)盡量減少鎖的交互數(shù)量,提前設(shè)計好鎖的順序并嚴格遵守。
結(jié)束語
并發(fā)編程系列基礎(chǔ)知識的學習到此結(jié)束了,后續(xù)如果遇到相關(guān)的知識再補充。
下一個系列《Java基礎(chǔ)》揚帆啟航,類加載、數(shù)據(jù)結(jié)構(gòu)(包括線程安全的數(shù)據(jù)結(jié)構(gòu))、泛型等知識將與你相遇。
祝大家圣誕節(jié)快樂!!