Java死鎖原因及預(yù)防方法超詳細(xì)講解
前言
Java 死鎖是多線程編程中一種經(jīng)典且棘手的問(wèn)題,它會(huì)導(dǎo)致多個(gè)線程相互等待對(duì)方持有的資源而永久阻塞。理解其產(chǎn)生原因和預(yù)防措施至關(guān)重要。
一、 Java 死鎖是如何產(chǎn)生的?
死鎖的發(fā)生需要同時(shí)滿(mǎn)足以下四個(gè)必要條件(缺一不可):
互斥使用 (Mutual Exclusion):
- 資源(如對(duì)象鎖、數(shù)據(jù)庫(kù)連接、文件句柄等)一次只能被一個(gè)線程獨(dú)占使用。
synchronized關(guān)鍵字或Lock對(duì)象實(shí)現(xiàn)的鎖機(jī)制本質(zhì)上就提供了這種互斥性。
持有并等待 (Hold and Wait / Partial Allocation):
- 一個(gè)線程在持有至少一個(gè)資源(鎖)的同時(shí),又去申請(qǐng)獲取另一個(gè)線程當(dāng)前正持有的資源(鎖)。
不可剝奪 (No Preemption):
- 一個(gè)線程已經(jīng)獲得的資源(鎖)在它主動(dòng)釋放之前,不能被其他線程強(qiáng)行剝奪。
- 在 Java 中,
synchronized鎖不能被強(qiáng)制中斷釋放;Lock.lock()獲取的鎖也不能被其他線程強(qiáng)制解鎖(除非使用Lock.lockInterruptibly()并中斷線程,但這通常也不是“強(qiáng)行剝奪”的含義)。
循環(huán)等待 (Circular Wait):
- 存在一組等待的線程
{T1, T2, ..., Tn},其中:- T1 等待 T2 持有的資源,
- T2 等待 T3 持有的資源,
- …,
- Tn 等待 T1 持有的資源。
- 所有線程形成一個(gè)等待資源的環(huán)。
- 存在一組等待的線程
經(jīng)典死鎖場(chǎng)景示例(哲學(xué)家就餐問(wèn)題簡(jiǎn)化版)
public class DeadlockExample {
static final Object lockA = new Object();
static final Object lockB = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lockA) { // 線程1獲取lockA
System.out.println("Thread1 acquired lockA");
try {
Thread.sleep(100); // 模擬操作,增加死鎖發(fā)生概率
} catch (InterruptedException e) {}
synchronized (lockB) { // 線程1嘗試獲取lockB(此時(shí)可能被線程2持有)
System.out.println("Thread1 acquired lockB");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lockB) { // 線程2獲取lockB
System.out.println("Thread2 acquired lockB");
try {
Thread.sleep(100); // 模擬操作,增加死鎖發(fā)生概率
} catch (InterruptedException e) {}
synchronized (lockA) { // 線程2嘗試獲取lockA(此時(shí)被線程1持有)
System.out.println("Thread2 acquired lockA");
}
}
});
thread1.start();
thread2.start();
}
}
分析死鎖條件滿(mǎn)足情況
- 互斥:
lockA和lockB都是synchronized使用的對(duì)象,具有互斥性。 - 持有并等待:
- 線程1 持有
lockA,同時(shí)等待獲取lockB。 - 線程2 持有
lockB,同時(shí)等待獲取lockA。
- 線程1 持有
- 不可剝奪: Java
synchronized鎖不能被其他線程強(qiáng)行剝奪。 - 循環(huán)等待:
- 線程1 在等待線程2 釋放的
lockB。 - 線程2 在等待線程1 釋放的
lockA。 - 形成了一個(gè)閉環(huán):線程1 -> 等待lockB(被線程2持有) -> 線程2 -> 等待lockA(被線程1持有) -> 線程1。
- 線程1 在等待線程2 釋放的
二、 如何防止 Java 死鎖?
防止死鎖的核心策略就是破壞上述四個(gè)必要條件中的至少一個(gè)。以下是常用的方法:
1. 破壞"循環(huán)等待"條件 - 鎖順序化 (Lock Ordering)
- 原理: 強(qiáng)制所有線程以全局一致的固定順序獲取鎖。
- 實(shí)現(xiàn):
- 為所有需要獲取的鎖定義一個(gè)全局的獲取順序(例如,按對(duì)象的
hashCode、按一個(gè)預(yù)定義的唯一ID、按名稱(chēng)排序等)。 - 在任何需要獲取多個(gè)鎖的地方,都嚴(yán)格按照這個(gè)全局順序去申請(qǐng)鎖。
- 為所有需要獲取的鎖定義一個(gè)全局的獲取順序(例如,按對(duì)象的
- 效果: 從根本上消除了循環(huán)等待的可能性。如果一個(gè)線程需要鎖 L1 和 L2,并且順序規(guī)定必須先 L1 后 L2,那么所有線程都會(huì)按這個(gè)順序申請(qǐng)。這樣就不會(huì)出現(xiàn)線程1 持 L1 等 L2,而線程2 持 L2 等 L1 的循環(huán)情況。
- 示例修改: 修改上面的例子,強(qiáng)制兩個(gè)線程都先獲取 lockA,再獲取 lockB。
Thread thread1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread1 acquired lockA");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockB) { // 總是先A后B
System.out.println("Thread1 acquired lockB");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lockA) { // 線程2也先嘗試獲取lockA
System.out.println("Thread2 acquired lockA");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockB) { // 再獲取lockB
System.out.println("Thread2 acquired lockB");
}
}
});
- 注意: 嚴(yán)格遵守順序是關(guān)鍵。有時(shí)確定一個(gè)一致的全局順序可能比較復(fù)雜(尤其是鎖是動(dòng)態(tài)創(chuàng)建或數(shù)量不確定時(shí)),但這是最推薦、最有效的預(yù)防策略之一??梢允褂?
System.identityHashCode(Object)作為最后手段來(lái)排序,但要注意哈希沖突。
2. 破壞"持有并等待"條件 - 一次性申請(qǐng)所有鎖 (Atomically Acquire All Locks)
- 原理: 一個(gè)線程在開(kāi)始執(zhí)行任務(wù)前,一次性申請(qǐng)它所需的所有鎖。如果無(wú)法一次性獲取全部鎖,它就不持有任何已獲得的鎖(全部釋放),等待一段時(shí)間再重試或采用其他策略。
- 實(shí)現(xiàn):
- 設(shè)計(jì)一個(gè)獲取多個(gè)鎖的機(jī)制(例如,一個(gè)包含所有需要鎖的集合)。
- 嘗試一次性獲取集合中所有的鎖(通常使用
tryLock)。 - 如果成功獲取所有鎖,執(zhí)行任務(wù)。
- 如果獲取任何一個(gè)鎖失?。ǔ瑫r(shí)或立即失敗),則釋放它已經(jīng)成功獲取的所有鎖,然后進(jìn)行回退(等待、重試、放棄任務(wù)等)。
- 效果: 線程要么同時(shí)持有所有需要的鎖(不等待),要么不持有任何鎖(不保持部分鎖去等待其他鎖),破壞了“持有并等待”。
- 工具: Java 的
Lock接口(特別是ReentrantLock)提供了tryLock()方法(可帶超時(shí))來(lái)實(shí)現(xiàn)這種細(xì)粒度控制,這比synchronized更靈活。 - 示例修改 (使用 ReentrantLock 和 tryLock):
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeadlockPrevention {
static Lock lockA = new ReentrantLock();
static Lock lockB = new ReentrantLock();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> acquireLocksAndWork(lockA, lockB, "Thread1"));
Thread thread2 = new Thread(() -> acquireLocksAndWork(lockB, lockA, "Thread2")); // 注意順序不同,但方法內(nèi)部處理
thread1.start();
thread2.start();
}
public static void acquireLocksAndWork(Lock firstLock, Lock secondLock, String threadName) {
while (true) {
boolean gotFirst = false;
boolean gotSecond = false;
try {
// 嘗試獲取第一個(gè)鎖(帶超時(shí)避免無(wú)限等待)
gotFirst = firstLock.tryLock(100, TimeUnit.MILLISECONDS);
if (gotFirst) {
System.out.println(threadName + " acquired first lock");
// 嘗試獲取第二個(gè)鎖(帶超時(shí))
gotSecond = secondLock.tryLock(100, TimeUnit.MILLISECONDS);
if (gotSecond) {
System.out.println(threadName + " acquired second lock");
// 成功獲取兩個(gè)鎖,執(zhí)行工作
System.out.println(threadName + " doing work...");
Thread.sleep(500); // 模擬工作
break; // 工作完成,跳出循環(huán)
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 無(wú)論如何,在退出前確保釋放已獲得的鎖
if (gotSecond) secondLock.unlock();
if (gotFirst) firstLock.unlock();
}
// 如果沒(méi)能一次性獲得兩個(gè)鎖,等待隨機(jī)時(shí)間后重試,避免活鎖
try {
Thread.sleep((long) (Math.random() * 100));
} catch (InterruptedException e) {}
}
}
}
3. 避免不必要的鎖 / 縮小鎖的范圍
- 原理: 減少鎖的持有時(shí)間和鎖的數(shù)量,從而降低線程在持有鎖期間去請(qǐng)求另一個(gè)鎖的機(jī)會(huì)(破壞持有并等待的機(jī)會(huì)),也減少了形成循環(huán)等待的可能性。
- 實(shí)現(xiàn):
- 只鎖必要的代碼塊: 盡可能縮小
synchronized塊的范圍,只保護(hù)真正需要互斥訪問(wèn)的共享數(shù)據(jù)操作。不要在鎖內(nèi)執(zhí)行耗時(shí)操作(如IO)。 - 使用線程安全類(lèi): 優(yōu)先使用
ConcurrentHashMap,CopyOnWriteArrayList,AtomicInteger等并發(fā)容器和原子類(lèi),它們內(nèi)部實(shí)現(xiàn)了高效的并發(fā)控制,減少了你顯式加鎖的需要。 - 不可變對(duì)象: 使用不可變對(duì)象(
final字段,構(gòu)造后狀態(tài)不變)。訪問(wèn)不可變對(duì)象不需要同步。 - 線程本地存儲(chǔ): 使用
ThreadLocal為每個(gè)線程創(chuàng)建變量的副本,避免共享。
- 只鎖必要的代碼塊: 盡可能縮小
- 效果: 雖然不是直接破壞必要條件,但這是良好的并發(fā)編程實(shí)踐,能顯著降低死鎖發(fā)生的概率和影響范圍。
4. 使用鎖超時(shí) (Lock Timeout) - 破壞"不可剝奪"的間接效果
- 原理: 在嘗試獲取鎖時(shí),不無(wú)限期等待,而是設(shè)置一個(gè)超時(shí)時(shí)間。如果超時(shí)還沒(méi)獲取到,則放棄當(dāng)前持有的所有鎖(如果需要),釋放資源,進(jìn)行回退(重試、記錄日志、失敗等)。
- 實(shí)現(xiàn): 主要依賴(lài)
Lock接口的tryLock(long time, TimeUnit unit)方法。synchronized無(wú)法直接實(shí)現(xiàn)超時(shí)。 - 效果: 它本身并不直接強(qiáng)行剝奪一個(gè)線程已持有的鎖(不破壞“不可剝奪”的本意),但它允許一個(gè)線程主動(dòng)放棄等待(等待超時(shí)),從而打破了死鎖環(huán)中等待的僵局。它破壞了死鎖發(fā)生的“永久阻塞”特性,給了系統(tǒng)恢復(fù)的機(jī)會(huì)。結(jié)合第2點(diǎn)(釋放已持有鎖),效果更好。
- 示例: 見(jiàn)上面第2點(diǎn)(一次性申請(qǐng)所有鎖)的代碼示例,其中就使用了
tryLock帶超時(shí)。
5. 死鎖檢測(cè)與恢復(fù)
- 原理: 不主動(dòng)預(yù)防死鎖,而是允許死鎖發(fā)生,但系統(tǒng)定期檢測(cè)死鎖的存在(如通過(guò)構(gòu)建資源分配圖并檢測(cè)環(huán)),一旦檢測(cè)到,采取強(qiáng)制措施打破死鎖(例如:終止一個(gè)或多個(gè)死鎖線程、剝奪其資源(在Java中很難安全實(shí)現(xiàn)))。
- Java 實(shí)現(xiàn):
- 檢測(cè): Java 沒(méi)有內(nèi)置的通用死鎖檢測(cè)API。但可以通過(guò)
ThreadMXBean的findDeadlockedThreads()或findMonitorDeadlockedThreads()方法來(lái)檢測(cè)由synchronized或ownable synchronizers(如ReentrantLock) 引起的死鎖。JMX 工具(如 JConsole, VisualVM)通常集成了這個(gè)功能。 - 恢復(fù): Java 本身沒(méi)有提供安全的、標(biāo)準(zhǔn)的線程終止或資源剝奪機(jī)制來(lái)恢復(fù)死鎖。通常檢測(cè)到死鎖后,只能記錄日志、告警,然后人工介入重啟應(yīng)用或相關(guān)服務(wù)。強(qiáng)行終止線程 (
Thread.stop()) 是極其危險(xiǎn)且已被廢棄的方法,會(huì)導(dǎo)致數(shù)據(jù)不一致等嚴(yán)重問(wèn)題,絕對(duì)不要使用。
- 檢測(cè): Java 沒(méi)有內(nèi)置的通用死鎖檢測(cè)API。但可以通過(guò)
- 應(yīng)用場(chǎng)景: 更適合框架、應(yīng)用服務(wù)器、數(shù)據(jù)庫(kù)等底層系統(tǒng)或需要高可靠性的復(fù)雜系統(tǒng),它們有更完善的資源管理和恢復(fù)機(jī)制。普通應(yīng)用開(kāi)發(fā)更應(yīng)注重預(yù)防。
總結(jié)與建議
- 首選鎖順序化: 在設(shè)計(jì)多鎖交互時(shí),強(qiáng)制全局一致的鎖獲取順序是最有效且推薦的預(yù)防策略。
- 善用 Lock 和 tryLock: 當(dāng)鎖順序難以嚴(yán)格保證或需要更靈活控制時(shí),使用
ReentrantLock及其tryLock(帶超時(shí))方法,實(shí)現(xiàn)一次性申請(qǐng)所有鎖或鎖超時(shí)機(jī)制。務(wù)必在 finally 塊中釋放鎖。 - 良好的并發(fā)習(xí)慣:
- 最小化鎖范圍(縮小
synchronized塊)。 - 優(yōu)先使用并發(fā)集合 (
java.util.concurrent.*) 和原子變量。 - 考慮不可變對(duì)象和線程本地存儲(chǔ) (
ThreadLocal)。
- 最小化鎖范圍(縮小
- 避免嵌套鎖: 盡量避免在一個(gè)鎖保護(hù)的代碼塊內(nèi)再去獲取另一個(gè)鎖。如果必須,嚴(yán)格應(yīng)用鎖順序化。
- 超時(shí)機(jī)制: 在可能長(zhǎng)時(shí)間等待的地方(包括鎖獲取、條件等待
Condition.await、線程join、Future.get等)使用超時(shí)參數(shù),防止永久阻塞,給系統(tǒng)提供回退的機(jī)會(huì)。 - 工具檢測(cè): 利用 JConsole、VisualVM、
jstack命令行工具等定期檢查或在線診斷潛在的死鎖。jstack -l <pid>輸出的線程轉(zhuǎn)儲(chǔ)會(huì)明確標(biāo)識(shí)出找到的死鎖和涉及的線程/鎖。
記住: 預(yù)防死鎖的關(guān)鍵在于設(shè)計(jì)和編碼階段就意識(shí)到風(fēng)險(xiǎn)并應(yīng)用上述策略。事后檢測(cè)和恢復(fù)往往是代價(jià)高昂的最后手段。??
到此這篇關(guān)于Java死鎖原因及預(yù)防方法的文章就介紹到這了,更多相關(guān)Java死鎖原因及預(yù)防內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決pageHelper分頁(yè)失效以及如何配置問(wèn)題
這篇文章主要介紹了解決pageHelper分頁(yè)失效以及如何配置問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04
聊聊spring @Transactional 事務(wù)無(wú)法使用的可能原因
這篇文章主要介紹了spring @Transactional 事務(wù)無(wú)法使用的可能原因,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
mybatis-plus返回map自動(dòng)轉(zhuǎn)駝峰配置操作
這篇文章主要介紹了mybatis-plus返回map自動(dòng)轉(zhuǎn)駝峰配置操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-11-11
IDEA Maven Mybatis generator 自動(dòng)生成代碼(實(shí)例講解)
下面小編就為大家分享一篇IDEA Maven Mybatis generator 自動(dòng)生成代碼的實(shí)例講解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2017-12-12
SpringBoot時(shí)區(qū)問(wèn)題解決以及徹底解決時(shí)差問(wèn)題
這篇文章主要給大家介紹了關(guān)于SpringBoot時(shí)區(qū)問(wèn)題解決以及徹底解決時(shí)差問(wèn)題的相關(guān)資料,spring?boot作為微服務(wù)簡(jiǎn)易架構(gòu),擁有其自身的特點(diǎn),快速搭建架構(gòu),簡(jiǎn)單快捷,需要的朋友可以參考下2023-08-08
Spring MVC Annotation驗(yàn)證的方法
這篇文章主要介紹了Spring MVC Annotation驗(yàn)證的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-03-03
java實(shí)現(xiàn)簡(jiǎn)單汽車(chē)租賃系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)簡(jiǎn)單汽車(chē)租賃系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-01-01

