Java死鎖原因及預(yù)防方法超詳細(xì)講解
前言
Java 死鎖是多線程編程中一種經(jīng)典且棘手的問題,它會(huì)導(dǎo)致多個(gè)線程相互等待對(duì)方持有的資源而永久阻塞。理解其產(chǎn)生原因和預(yù)防措施至關(guān)重要。
一、 Java 死鎖是如何產(chǎn)生的?
死鎖的發(fā)生需要同時(shí)滿足以下四個(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é)家就餐問題簡(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(); } }
分析死鎖條件滿足情況
- 互斥:
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、按名稱排序等)。 - 在任何需要獲取多個(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)
作為最后手段來排序,但要注意哈希沖突。
2. 破壞"持有并等待"條件 - 一次性申請(qǐng)所有鎖 (Atomically Acquire All Locks)
- 原理: 一個(gè)線程在開始執(zhí)行任務(wù)前,一次性申請(qǐng)它所需的所有鎖。如果無法一次性獲取全部鎖,它就不持有任何已獲得的鎖(全部釋放),等待一段時(shí)間再重試或采用其他策略。
- 實(shí)現(xiàn):
- 設(shè)計(jì)一個(gè)獲取多個(gè)鎖的機(jī)制(例如,一個(gè)包含所有需要鎖的集合)。
- 嘗試一次性獲取集合中所有的鎖(通常使用
tryLock
)。 - 如果成功獲取所有鎖,執(zhí)行任務(wù)。
- 如果獲取任何一個(gè)鎖失敗(超時(shí)或立即失?。?,則釋放它已經(jīng)成功獲取的所有鎖,然后進(jìn)行回退(等待、重試、放棄任務(wù)等)。
- 效果: 線程要么同時(shí)持有所有需要的鎖(不等待),要么不持有任何鎖(不保持部分鎖去等待其他鎖),破壞了“持有并等待”。
- 工具: Java 的
Lock
接口(特別是ReentrantLock
)提供了tryLock()
方法(可帶超時(shí))來實(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í)避免無限等待) 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 { // 無論如何,在退出前確保釋放已獲得的鎖 if (gotSecond) secondLock.unlock(); if (gotFirst) firstLock.unlock(); } // 如果沒能一次性獲得兩個(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ù)真正需要互斥訪問的共享數(shù)據(jù)操作。不要在鎖內(nèi)執(zhí)行耗時(shí)操作(如IO)。 - 使用線程安全類: 優(yōu)先使用
ConcurrentHashMap
,CopyOnWriteArrayList
,AtomicInteger
等并發(fā)容器和原子類,它們內(nèi)部實(shí)現(xiàn)了高效的并發(fā)控制,減少了你顯式加鎖的需要。 - 不可變對(duì)象: 使用不可變對(duì)象(
final
字段,構(gòu)造后狀態(tài)不變)。訪問不可變對(duì)象不需要同步。 - 線程本地存儲(chǔ): 使用
ThreadLocal
為每個(gè)線程創(chuàng)建變量的副本,避免共享。
- 只鎖必要的代碼塊: 盡可能縮小
- 效果: 雖然不是直接破壞必要條件,但這是良好的并發(fā)編程實(shí)踐,能顯著降低死鎖發(fā)生的概率和影響范圍。
4. 使用鎖超時(shí) (Lock Timeout) - 破壞"不可剝奪"的間接效果
- 原理: 在嘗試獲取鎖時(shí),不無限期等待,而是設(shè)置一個(gè)超時(shí)時(shí)間。如果超時(shí)還沒獲取到,則放棄當(dāng)前持有的所有鎖(如果需要),釋放資源,進(jìn)行回退(重試、記錄日志、失敗等)。
- 實(shí)現(xiàn): 主要依賴
Lock
接口的tryLock(long time, TimeUnit unit)
方法。synchronized
無法直接實(shí)現(xiàn)超時(shí)。 - 效果: 它本身并不直接強(qiáng)行剝奪一個(gè)線程已持有的鎖(不破壞“不可剝奪”的本意),但它允許一個(gè)線程主動(dòng)放棄等待(等待超時(shí)),從而打破了死鎖環(huán)中等待的僵局。它破壞了死鎖發(fā)生的“永久阻塞”特性,給了系統(tǒng)恢復(fù)的機(jī)會(huì)。結(jié)合第2點(diǎn)(釋放已持有鎖),效果更好。
- 示例: 見上面第2點(diǎn)(一次性申請(qǐng)所有鎖)的代碼示例,其中就使用了
tryLock
帶超時(shí)。
5. 死鎖檢測(cè)與恢復(fù)
- 原理: 不主動(dòng)預(yù)防死鎖,而是允許死鎖發(fā)生,但系統(tǒng)定期檢測(cè)死鎖的存在(如通過構(gòu)建資源分配圖并檢測(cè)環(huán)),一旦檢測(cè)到,采取強(qiáng)制措施打破死鎖(例如:終止一個(gè)或多個(gè)死鎖線程、剝奪其資源(在Java中很難安全實(shí)現(xiàn)))。
- Java 實(shí)現(xiàn):
- 檢測(cè): Java 沒有內(nèi)置的通用死鎖檢測(cè)API。但可以通過
ThreadMXBean
的findDeadlockedThreads()
或findMonitorDeadlockedThreads()
方法來檢測(cè)由synchronized
或ownable synchronizers
(如ReentrantLock
) 引起的死鎖。JMX 工具(如 JConsole, VisualVM)通常集成了這個(gè)功能。 - 恢復(fù): Java 本身沒有提供安全的、標(biāo)準(zhǔn)的線程終止或資源剝奪機(jī)制來恢復(fù)死鎖。通常檢測(cè)到死鎖后,只能記錄日志、告警,然后人工介入重啟應(yīng)用或相關(guān)服務(wù)。強(qiáng)行終止線程 (
Thread.stop()
) 是極其危險(xiǎn)且已被廢棄的方法,會(huì)導(dǎo)致數(shù)據(jù)不一致等嚴(yán)重問題,絕對(duì)不要使用。
- 檢測(cè): Java 沒有內(nèi)置的通用死鎖檢測(cè)API。但可以通過
- 應(yīng)用場(chǎng)景: 更適合框架、應(yīng)用服務(wù)器、數(shù)據(jù)庫(kù)等底層系統(tǒng)或需要高可靠性的復(fù)雜系統(tǒng),它們有更完善的資源管理和恢復(fù)機(jī)制。普通應(yīng)用開發(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í)出找到的死鎖和涉及的線程/鎖。
記?。?/strong> 預(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)文章
Spring中的@Qualifier注解和@Resource注解區(qū)別解析
這篇文章主要介紹了Spring中的@Qualifier注解和@Resource注解區(qū)別解析,@Qualifier注解的用處是當(dāng)一個(gè)接口有多個(gè)實(shí)現(xiàn)的時(shí)候,為了指名具體調(diào)用哪個(gè)類的實(shí)現(xiàn),@Resource注解可以通過 byName命名和byType類型的方式注入,需要的朋友可以參考下2023-11-11Java mysql詳細(xì)講解雙數(shù)據(jù)源配置使用
在開發(fā)過程中我們常常會(huì)用到兩個(gè)數(shù)據(jù)庫(kù),一個(gè)數(shù)據(jù)用來實(shí)現(xiàn)一些常規(guī)的增刪改查,另外一個(gè)數(shù)據(jù)庫(kù)用來實(shí)時(shí)存數(shù)據(jù)。進(jìn)行數(shù)據(jù)的統(tǒng)計(jì)分析??梢宰x寫分離。可以更好的優(yōu)化和提高效率;或者兩個(gè)數(shù)據(jù)存在業(yè)務(wù)分離的時(shí)候也需要多個(gè)數(shù)據(jù)源來實(shí)現(xiàn)2022-06-06Java中ArrayList和LinkedList之間的區(qū)別_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了Java中ArrayList和LinkedList之間的區(qū)別,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05SpringBoot+Hutool+thymeleaf完成導(dǎo)出Excel的實(shí)現(xiàn)方法
這篇文章主要介紹了SpringBoot+Hutool+thymeleaf完成導(dǎo)出Excel,本篇示例當(dāng)中不僅僅有后端,而且還提供了前端html,html當(dāng)中利用js將后端 輸出流直接下載為文件,需要的朋友可以參考下2022-03-03SpringBoot配置Redis實(shí)現(xiàn)保存獲取和刪除數(shù)據(jù)
本文主要介紹了SpringBoot配置Redis實(shí)現(xiàn)保存獲取和刪除數(shù)據(jù),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,感興趣的小伙伴們可以參考一下2021-06-06java中TCP實(shí)現(xiàn)回顯服務(wù)器及客戶端
本文主要介紹了java中TCP實(shí)現(xiàn)回顯服務(wù)器及客戶端,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02使用@CachePut?更新數(shù)據(jù)庫(kù)和更新緩存
這篇文章主要介紹了使用@CachePut?更新數(shù)據(jù)庫(kù)和更新緩存方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12Spring線程池ThreadPoolTaskExecutor的用法及說明
這篇文章主要介紹了Spring線程池ThreadPoolTaskExecutor的用法及說明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07