Java并發(fā)常見問題之死鎖/活鎖/饑餓的排查與解決方法
一、前言
在多線程并發(fā)編程中,除了數(shù)據(jù)安全問題,線程協(xié)作異常是另一類高頻問題,其中死鎖、活鎖、饑餓是最典型的三類問題。這些問題會導致線程無法正常執(zhí)行、系統(tǒng)性能下降甚至服務不可用,且排查難度高 —— 死鎖可能隱藏數(shù)月,在高并發(fā)場景下才會觸發(fā)。
本文將深入剖析這三類問題的產(chǎn)生原因、典型場景、排查方法,并提供可落地的解決方案與避坑指南。
二、死鎖(Deadlock)
1. 死鎖的定義
死鎖是指兩個或多個線程互相持有對方所需的鎖,且都不釋放自己持有的鎖,導致所有線程永久阻塞,無法繼續(xù)執(zhí)行的狀態(tài)。
2. 死鎖的四大必要條件(缺一不可)
只有同時滿足以下 4 個條件,才會產(chǎn)生死鎖:
互斥條件:鎖資源只能被一個線程持有,其他線程無法獲取;
持有并等待條件:線程持有已獲取的鎖,同時等待其他線程持有的鎖;
不可剝奪條件:線程持有的鎖不能被強制剝奪,只能由線程主動釋放;
循環(huán)等待條件:線程 A 等待線程 B 的鎖,線程 B 等待線程 A 的鎖,形成循環(huán)等待鏈。
3. 死鎖典型案例
/**
* 死鎖演示:線程1持有鎖A,等待鎖B;線程2持有鎖B,等待鎖A
*/
public class DeadlockDemo {
// 定義兩個鎖對象
private static final Object LOCK_A = new Object();
private static final Object LOCK_B = new Object();
public static void main(String[] args) {
// 線程1:獲取LOCK_A → 等待LOCK_B
new Thread(() -> {
synchronized (LOCK_A) {
System.out.println(Thread.currentThread().getName() + " 獲取到LOCK_A,等待LOCK_B");
try {
Thread.sleep(1000); // 放大死鎖概率
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LOCK_B) {
System.out.println(Thread.currentThread().getName() + " 獲取到LOCK_B,執(zhí)行完成");
}
}
}, "線程1").start();
// 線程2:獲取LOCK_B → 等待LOCK_A
new Thread(() -> {
synchronized (LOCK_B) {
System.out.println(Thread.currentThread().getName() + " 獲取到LOCK_B,等待LOCK_A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LOCK_A) {
System.out.println(Thread.currentThread().getName() + " 獲取到LOCK_A,執(zhí)行完成");
}
}
}, "線程2").start();
}
}運行結(jié)果 :兩個線程互相等待對方的鎖,永久阻塞,無后續(xù)輸出。
4. 死鎖的排查方法
方法 1:jstack 命令(最常用)
1.執(zhí)行 jps 命令,獲取進程 ID:
jps # 輸出示例:1234 DeadlockDemo
2.執(zhí)行 jstack <進程ID> ,查看線程狀態(tài):
jstack 1234
3.關(guān)鍵輸出(死鎖提示):
Found one Java-level deadlock: ============================= "線程2": waiting to lock monitor 0x00007f9e3c006800 (object 0x000000076ab60eb0, a java.lang.Object), which is held by "線程1" "線程1": waiting to lock monitor 0x00007f9e3c009000 (object 0x000000076ab60ec0, a java.lang.Object), which is held by "線程2"
方法 2:JConsole 可視化工具
啟動 JConsole(JDK/bin 目錄下),連接目標進程;
切換到「線程」標簽頁,點擊「檢測死鎖」,自動識別死鎖線程及鎖信息。
5. 死鎖的解決方案
核心思路: 破壞死鎖的四大必要條件之一 ,常用方案:
方案 1:統(tǒng)一鎖獲取順序(破壞循環(huán)等待條件)
所有線程按相同的順序獲取鎖,避免循環(huán)等待:
// 優(yōu)化后:線程1和線程2都先獲取LOCK_A,再獲取LOCK_B
new Thread(() -> {
synchronized (LOCK_A) {
System.out.println(Thread.currentThread().getName() + " 獲取到LOCK_A,等待LOCK_B");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LOCK_B) {
System.out.println(Thread.currentThread().getName() + " 獲取到LOCK_B,執(zhí)行完成");
}
}
}, "線程1").start();
new Thread(() -> {
synchronized (LOCK_A) { // 統(tǒng)一先獲取LOCK_A
System.out.println(Thread.currentThread().getName() + " 獲取到LOCK_A,等待LOCK_B");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LOCK_B) {
System.out.println(Thread.currentThread().getName() + " 獲取到LOCK_B,執(zhí)行完成");
}
}
}, "線程2").start();方案 2:使用可中斷鎖(破壞不可剝奪條件)
使用 ReentrantLock 的 lockInterruptibly() 方法,允許線程被中斷,主動釋放鎖:
import java.util.concurrent.locks.ReentrantLock;
public class DeadlockResolveByInterrupt {
private static final ReentrantLock LOCK_A = new ReentrantLock();
private static final ReentrantLock LOCK_B = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
try {
// 可中斷的鎖獲取
LOCK_A.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + " 獲取到LOCK_A,等待LOCK_B");
Thread.sleep(1000);
LOCK_B.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + " 獲取到LOCK_B,執(zhí)行完成");
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 被中斷,釋放鎖");
if (LOCK_A.isHeldByCurrentThread()) {
LOCK_A.unlock();
}
if (LOCK_B.isHeldByCurrentThread()) {
LOCK_B.unlock();
}
}
}, "線程1");
Thread thread2 = new Thread(() -> {
try {
LOCK_B.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + " 獲取到LOCK_B,等待LOCK_A");
Thread.sleep(1000);
LOCK_A.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + " 獲取到LOCK_A,執(zhí)行完成");
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 被中斷,釋放鎖");
if (LOCK_A.isHeldByCurrentThread()) {
LOCK_A.unlock();
}
if (LOCK_B.isHeldByCurrentThread()) {
LOCK_B.unlock();
}
}
}, "線程2");
thread1.start();
thread2.start();
// 等待3秒,若檢測到死鎖,中斷線程1
Thread.sleep(3000);
if (thread1.isAlive() && thread2.isAlive()) {
thread1.interrupt();
System.out.println("檢測到死鎖,中斷線程1");
}
}
}方案 3:使用超時獲取鎖(破壞持有并等待條件)
使用 ReentrantLock 的 tryLock(long time, TimeUnit unit) 方法,超時未獲取鎖則放棄,避免永久等待:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class DeadlockResolveByTimeout {
private static final ReentrantLock LOCK_A = new ReentrantLock();
private static final ReentrantLock LOCK_B = new ReentrantLock();
public static void main(String[] args) {
new Thread(() -> {
try {
if (LOCK_A.tryLock(2, TimeUnit.SECONDS)) { // 超時2秒
System.out.println(Thread.currentThread().getName() + " 獲取到LOCK_A,等待LOCK_B");
Thread.sleep(1000);
if (LOCK_B.tryLock(2, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + " 獲取到LOCK_B,執(zhí)行完成");
LOCK_B.unlock();
} else {
System.out.println(Thread.currentThread().getName() + " 超時未獲取LOCK_B,釋放LOCK_A");
LOCK_A.unlock();
}
LOCK_A.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "線程1").start();
new Thread(() -> {
try {
if (LOCK_B.tryLock(2, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + " 獲取到LOCK_B,等待LOCK_A");
Thread.sleep(1000);
if (LOCK_A.tryLock(2, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + " 獲取到LOCK_A,執(zhí)行完成");
LOCK_A.unlock();
} else {
System.out.println(Thread.currentThread().getName() + " 超時未獲取LOCK_A,釋放LOCK_B");
LOCK_B.unlock();
}
LOCK_B.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "線程2").start();
}
}三、活鎖(Livelock)
1. 活鎖的定義
活鎖是指線程沒有阻塞,但因互相謙讓(或重試),導致始終無法獲取所需資源,程序無法推進的狀態(tài)。與死鎖的核心區(qū)別:死鎖是線程完全阻塞,活鎖是線程一直在執(zhí)行,但無實際進展。
2. 活鎖典型案例
/**
* 活鎖演示:兩個線程互相釋放鎖,重試獲取對方的鎖,始終無法執(zhí)行完成
*/
public class LivelockDemo {
private static final ReentrantLock LOCK_A = new ReentrantLock();
private static final ReentrantLock LOCK_B = new ReentrantLock();
public static void main(String[] args) {
// 線程1:獲取LOCK_A失敗 → 釋放LOCK_B(若持有)→ 重試
new Thread(() -> {
while (true) {
try {
if (LOCK_A.tryLock(100, TimeUnit.MILLISECONDS)) {
System.out.println(Thread.currentThread().getName() + " 獲取到LOCK_A,嘗試獲取LOCK_B");
if (LOCK_B.tryLock(100, TimeUnit.MILLISECONDS)) {
System.out.println(Thread.currentThread().getName() + " 獲取到LOCK_B,執(zhí)行完成");
LOCK_B.unlock();
LOCK_A.unlock();
break; // 執(zhí)行完成,退出循環(huán)
} else {
System.out.println(Thread.currentThread().getName() + " 獲取LOCK_B失敗,釋放LOCK_A");
LOCK_A.unlock();
Thread.sleep(100); // 謙讓,重試
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "線程1").start();
// 線程2:獲取LOCK_B失敗 → 釋放LOCK_A(若持有)→ 重試
new Thread(() -> {
while (true) {
try {
if (LOCK_B.tryLock(100, TimeUnit.MILLISECONDS)) {
System.out.println(Thread.currentThread().getName() + " 獲取到LOCK_B,嘗試獲取LOCK_A");
if (LOCK_A.tryLock(100, TimeUnit.MILLISECONDS)) {
System.out.println(Thread.currentThread().getName() + " 獲取到LOCK_A,執(zhí)行完成");
LOCK_A.unlock();
LOCK_B.unlock();
break;
} else {
System.out.println(Thread.currentThread().getName() + " 獲取LOCK_A失敗,釋放LOCK_B");
LOCK_B.unlock();
Thread.sleep(100); // 謙讓,重試
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "線程2").start();
}
}運行結(jié)果 :兩個線程反復獲取鎖、釋放鎖、重試,始終無法同時獲取兩個鎖,陷入無限循環(huán)。
3. 活鎖的解決方案
核心思路: 打破 “同步重試” 的循環(huán) ,常用方案:
1.隨機重試延遲:每個線程重試時使用隨機的休眠時間,避免同步謙讓;
// 替換固定休眠時間為隨機時間 Thread.sleep(new Random().nextInt(500));
2.優(yōu)先級機制:為線程設置不同的優(yōu)先級,讓部分線程優(yōu)先獲取資源;
3.限制重試次數(shù):設置最大重試次數(shù),超過次數(shù)則放棄并報警,避免無限循環(huán)。
四、饑餓(Starvation)
1. 饑餓的定義
饑餓是指某些線程因優(yōu)先級低、或始終競爭不到鎖資源,導致長期無法執(zhí)行的狀態(tài)。例如:高優(yōu)先級線程持續(xù)占用 CPU,低優(yōu)先級線程始終無法執(zhí)行;非公平鎖下,某些線程始終搶不到鎖。
2. 饑餓典型案例
/**
* 饑餓演示:高優(yōu)先級線程持續(xù)占用鎖,低優(yōu)先級線程長期無法獲取鎖
*/
public class StarvationDemo {
private static final Object LOCK = new Object();
public static void main(String[] args) {
// 低優(yōu)先級線程
Thread lowPriorityThread = new Thread(() -> {
int count = 0;
while (true) {
synchronized (LOCK) {
System.out.println(Thread.currentThread().getName() + " 執(zhí)行第" + (++count) + "次");
try {
Thread.sleep(100); // 持有鎖時間短,但仍被高優(yōu)先級線程搶占
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "低優(yōu)先級線程");
lowPriorityThread.setPriority(Thread.MIN_PRIORITY); // 優(yōu)先級1
// 高優(yōu)先級線程
Thread highPriorityThread = new Thread(() -> {
int count = 0;
while (true) {
synchronized (LOCK) {
System.out.println(Thread.currentThread().getName() + " 執(zhí)行第" + (++count) + "次");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "高優(yōu)先級線程");
highPriorityThread.setPriority(Thread.MAX_PRIORITY); // 優(yōu)先級10
lowPriorityThread.start();
highPriorityThread.start();
}
}運行結(jié)果 :高優(yōu)先級線程的執(zhí)行次數(shù)遠多于低優(yōu)先級線程,低優(yōu)先級線程長期 “饑餓”。
3. 饑餓的解決方案
核心思路: 保證資源分配的公平性 ,常用方案:
1.使用公平鎖: ReentrantLock 的公平鎖模式保證線程按 FIFO 順序獲取鎖,避免插隊;
private static final ReentrantLock LOCK = new ReentrantLock(true); // 公平鎖
2.避免線程優(yōu)先級差異:盡量將線程優(yōu)先級設置為相同(默認 5),減少調(diào)度器的偏好;
3.減少鎖持有時間:縮短同步代碼塊的執(zhí)行時間,讓鎖盡快釋放,增加低優(yōu)先級線程的獲取機會;
4.使用線程池:線程池的工作線程優(yōu)先級一致,且有任務隊列緩沖,避免個別線程長期搶占資源。
五、并發(fā)問題對比
問題類型 | 核心特征 | 線程狀態(tài) | 排查難度 | 核心解決方案 |
|---|---|---|---|---|
死鎖 | 互相持有鎖,永久阻塞 | BLOCKED | 中(jstack 可直接檢測) | 統(tǒng)一鎖順序、超時獲取、可中斷鎖 |
活鎖 | 無阻塞,但互相謙讓,無進展 | RUNNABLE | 高(無明顯報錯,需分析日志) | 隨機重試延遲、優(yōu)先級機制、限制重試次數(shù) |
饑餓 | 長期競爭不到資源,偶爾執(zhí)行 | RUNNABLE | 中(需統(tǒng)計執(zhí)行頻率) | 公平鎖、統(tǒng)一優(yōu)先級、減少鎖持有時間 |
六、實戰(zhàn)避坑指南
1. 預防死鎖的最佳實踐
最小化鎖范圍:僅在必要的代碼塊加鎖,縮短鎖持有時間;
避免嵌套鎖:盡量不使用多層鎖嵌套,若必須使用,嚴格統(tǒng)一鎖獲取順序;
使用定時鎖:優(yōu)先使用 tryLock(timeout) 替代無超時的鎖獲取;
監(jiān)控鎖狀態(tài):通過 JMX/APM 工具監(jiān)控鎖的持有時間、競爭次數(shù),提前發(fā)現(xiàn)死鎖風險。
2. 通用優(yōu)化建議
優(yōu)先使用并發(fā)工具: ConcurrentHashMap 、 CountDownLatch 等工具已封裝安全的并發(fā)邏輯,避免手動加鎖;
避免手動線程管理:使用線程池( ThreadPoolExecutor )替代手動創(chuàng)建線程,統(tǒng)一管理線程生命周期;
增加容錯機制:關(guān)鍵業(yè)務線程設置超時、重試、降級邏輯,避免因并發(fā)問題導致服務不可用;
壓測驗證:上線前通過高并發(fā)壓測,模擬極端場景,提前暴露死鎖 / 活鎖 / 饑餓問題。
七、總結(jié)
本文深入剖析了 Java 并發(fā)編程中死鎖、活鎖、饑餓三類典型問題的產(chǎn)生原因、典型場景與解決方案。死鎖是最致命的問題,需通過破壞四大必要條件來預防;活鎖需打破同步重試的循環(huán);饑餓需保證資源分配的公平性。在實際開發(fā)中,應遵循 “預防大于排查” 的原則:通過統(tǒng)一鎖順序、使用公平鎖、縮短鎖持有時間等手段,從根源減少并發(fā)問題的發(fā)生;同時掌握 jstack、JConsole 等排查工具,快速定位已出現(xiàn)的問題。
到此這篇關(guān)于Java并發(fā)常見問題之死鎖/活鎖/饑餓的排查與解決方法的文章就介紹到這了,更多相關(guān)Java并發(fā)死鎖/活鎖/饑餓內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
spring mvc 實現(xiàn)獲取后端傳遞的值操作示例
這篇文章主要介紹了spring mvc 實現(xiàn)獲取后端傳遞的值操作,結(jié)合實例形式詳細分析了spring mvc使用JSTL 方法獲取后端傳遞的值相關(guān)操作技巧2019-11-11
Java Swing中JList選擇事件監(jiān)聽器ListSelectionListener用法示例
這篇文章主要介紹了Java Swing中JList選擇事件監(jiān)聽器ListSelectionListener用法,結(jié)合具體實例形式分析了中JList選擇事件監(jiān)聽器ListSelectionListener的功能、使用方法及相關(guān)注意事項,需要的朋友可以參考下2017-11-11
java數(shù)據(jù)庫連接池和數(shù)據(jù)庫連接示例
這篇文章主要介紹了java數(shù)據(jù)庫連接池和數(shù)據(jù)庫連接示例,需要的朋友可以參考下2014-05-05
MyBatis關(guān)閉一級緩存的兩種方式(分注解和xml兩種方式)
這篇文章主要介紹了MyBatis關(guān)閉一級緩存的兩種方式(分注解和xml兩種方式),mybatis默認開啟一級緩存,執(zhí)行2次相同sql,但是第一次查詢sql結(jié)果會加工處理這個時候需要關(guān)閉一級緩存,本文給大家詳細講解需要的朋友可以參考下2022-11-11
Java Integer.valueOf()和Integer.parseInt()的區(qū)別說明
這篇文章主要介紹了Java Integer.valueOf()和Integer.parseInt()的區(qū)別說明,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-08-08

