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

