Java程序死鎖問題定位與解決方法
1. 死鎖概述
1.1 什么是死鎖
- 一定是發(fā)生在并發(fā)中;
- 互不相讓:當兩個(或更多)線程(或進程)相互持有對方所要的資源,又不主動釋放,導致程序陷入無盡的阻塞,這就是死鎖。

1.2 死鎖產(chǎn)生的必要條件
導致死鎖的條件有四個,這四個條件同時滿足就會產(chǎn)生死鎖。
- 互斥條件:某些資源只能由一個線程獨占使用,其他線程在資源被占用時只能等待。
- 請求和保持條件:一個線程因請求資源而阻塞時,對已獲得的資源保持不放。
- 不可搶占條件:線程已獲得的資源,在未使用完之前,不能強行剝奪。
- 循環(huán)等待條件:若干線程之間形成一種頭尾相接的循環(huán)等待資源關系。
2. 死鎖的案例分析
public class Resource {
private String name;
private int count;
public Resource(String name) {
this.name = name;
}
public void staticResource() {
synchronized (this) {
System.out.println("static resource");
count++;
}
}
public void saveResource(Resource resource) {
synchronized (this) {
System.out.println("save resource:" + Thread.currentThread().getName());
resource.staticResource();
}
}
}
public class DeadLock {
public static void main(String[] args) {
Resource resource1 = new Resource("resource1");
Resource resource2 = new Resource("resource2");
Thread threadA = new Thread(() -> {
for (int i = 0; i < 100; i++) {
resource1.saveResource(resource2);
}
});
Thread threadB = new Thread(() -> {
for (int i = 0; i < 100; i++) {
resource2.saveResource(resource1);
}
});
threadA.start();
threadB.start();
}
}
打印結(jié)果:
save resource:Thread-0 save resource:Thread-1
死鎖原因分析:
- 線程 A 行為:
threadA在調(diào)用resource1.saveResource(resource2)時:- 首先鎖住了
resource1對象。 - 然后試圖鎖住
resource2對象,進入其staticResource方法。
- 首先鎖住了
- 線程B行為:
threadB在調(diào)用resource2.saveResource(resource1)時:- 首先鎖住了
resource2對象。 - 然后試圖鎖住
resource1對象,進入其staticResource方法。
- 首先鎖住了
- 死鎖發(fā)生的原因:
- 如果
threadA已經(jīng)鎖住resource1,并等待鎖住resource2,而此時threadB已經(jīng)鎖住resource2并等待鎖住resource1,就會發(fā)生循環(huán)等待。 - 兩個線程互相等待對方釋放鎖,從而陷入死鎖狀態(tài)。
3. 死鎖排查
- 首先,通過
jps命令,查看 Java 進程的 pid。
C:\Users\shawn>jps 22568 24488 Launcher 10060 DeadLock 28076 Jps
- 然后,通過
jstack <pid>命令查看線程 dump 日志。當發(fā)現(xiàn)死鎖時,可以在打印的 dump 日志中找到Found one Java-level deadlock:信息,根據(jù)信息的內(nèi)容可以分析死鎖出現(xiàn)的原因。
C:\Users\shawn>jstack 23128
2024-11-23 15:38:34
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.321-b07 mixed mode):
=============================
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x0000022b6c713f08 (object 0x000000076bdaa990, a com.atu.deadlock.Resource),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x0000022b6c7169a8 (object 0x000000076bdaa9e8, a com.atu.deadlock.Resource),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at com.atu.deadlock.Resource.staticResource(Resource.java:13)
- waiting to lock <0x000000076bdaa990> (a com.atu.deadlock.Resource)
at com.atu.deadlock.Resource.saveResource(Resource.java:21)
- locked <0x000000076bdaa9e8> (a com.atu.deadlock.Resource)
at com.atu.deadlock.DeadLock.lambda$main$1(DeadLock.java:18)
at com.atu.deadlock.DeadLock$$Lambda$2/1096979270.run(Unknown Source)
at java.lang.Thread.run(Thread.java:750)
"Thread-0":
at com.atu.deadlock.Resource.staticResource(Resource.java:13)
- waiting to lock <0x000000076bdaa9e8> (a com.atu.deadlock.Resource)
at com.atu.deadlock.Resource.saveResource(Resource.java:21)
- locked <0x000000076bdaa990> (a com.atu.deadlock.Resource)
at com.atu.deadlock.DeadLock.lambda$main$0(DeadLock.java:12)
at com.atu.deadlock.DeadLock$$Lambda$1/1324119927.run(Unknown Source)
at java.lang.Thread.run(Thread.java:750)
Found 1 deadlock.
4. 線上發(fā)生死鎖應該怎么辦
- 首先保存案發(fā)現(xiàn)場,然后立刻重啟服務器(使用 java 相應的命令把整個堆棧信息保存下來),不能進一步影響用戶體驗;
- 暫時保證線上服務的安全,然后再利用剛才保存的信息,排查死鎖,修改代碼,重新發(fā)版。
5. 常見死鎖修復策略
前面我們說死鎖的四個必要條件,我們只需要破壞其中任意一個,就可以避免死鎖的產(chǎn)生。其中,互斥條件我們不可以破壞,因為這是互斥鎖的基本約束,其他三個條件都可以破壞。
- 破壞請求和保持條件:線程在請求開始前,一次性申請所有的資源。
- 破壞不可搶占條件:占用部分資源的線程進一步申請其他資源時,如果申請不到,可以主動釋放它占有的資源。
- 破壞循環(huán)等待條件:靠按序申請資源來預防。按某一順序申請資源,釋放資源則反序釋放。破壞循環(huán)等待條件。
5.1 破壞請求和保持條件
要破壞占用資源所帶來的等待,可以一次性申請所有資源,保證同時申請這個操作是在一個臨界區(qū)中,然后通過一個單獨的角色來管理這個臨界區(qū)。
- 這個角色有兩個很重要的功能,就是同時申請資源和同時釋放資源,并且這個角色一定是一個單例。
先定義一個 ApplyLock 類,用來實現(xiàn)統(tǒng)一鎖資源的申請,該類中有兩個方法:
- 一個是
applyLock()方法,用來申請鎖; - 另一個是
free()方法,用來統(tǒng)一釋放鎖。
public class ApplyLock {
private List<Object> list = new ArrayList<>();
public synchronized boolean applyLock(Resource resource1, Resource resource2) {
if (list.contains(resource1) || list.contains(resource2)) {
return false;
} else {
list.add(resource1);
list.add(resource2);
return true;
}
}
public synchronized void free(Resource resource1, Resource resource2) {
list.remove(resource1);
list.remove(resource2);
}
}
修改 Resource 類,定義一個全局唯一的 ApplyLock 實例,然后在 saveResource 中調(diào)用 applyLock() 方法和 free() 方法進行統(tǒng)一鎖資源的獲取和釋放。
public class Resource {
private String name;
private int count;
static ApplyLock applyLock = new ApplyLock();
public Resource(String name) {
this.name = name;
}
public void staticResource() {
synchronized (this) {
System.out.println("static resource");
count++;
}
}
public void saveResource(Resource resource) {
applyLock.applyLock(this, resource);
try {
System.out.println("save resource:" + Thread.currentThread().getName());
resource.staticResource();
} finally {
applyLock.free(this, resource);
}
}
}
由于當前涉及的相關資源都實現(xiàn)了一個統(tǒng)一的鎖資源獲取和釋放,從而打破了請求和保持條件。
5.2 破壞不可搶占條件
破壞不可搶占條件的核心是當前線程能夠主動釋放嘗試占有的資源,這一點 synchronized無法實現(xiàn)。
- 原因是
synchronized在申請不到資源時會直接進入阻塞狀態(tài),一旦線程被阻塞就無法再釋放已經(jīng)占有的資源。 - 在
java.util.concurrent包中的Lock鎖可以輕松地解決這個問題。Lock接口中有一個tryLock()方法可以嘗試搶占資源,如果搶占成功則返回 true,否則返回 false,而且這個過程不會阻塞當前線程。
import java.util.concurrent.locks.ReentrantLock;
public class Resource {
private String name;
private int count;
ReentrantLock lock = new ReentrantLock();
public Resource(String name) {
this.name = name;
}
public void staticResource() {
if (lock.tryLock()) {
try {
System.out.println("static resource");
count++;
} finally {
lock.unlock();
}
} else {
System.out.println("搶鎖失敗");
}
}
public void saveResource(Resource resource) {
if (lock.tryLock()) {
try {
System.out.println("save resource:" + Thread.currentThread().getName());
resource.staticResource();
} finally {
lock.unlock();
}
} else {
System.out.println("搶鎖失敗");
}
}
}
5.3 破壞循環(huán)等待條件
破壞循環(huán)等待條件的基本思想是:把資源按照某種順序編號,所有鎖資源的申請都按照某種順序來獲取。 比如,可以根據(jù) hashCode 來確定加鎖順序,再根據(jù) hashCode 的大小確定加鎖的對象,實現(xiàn)代碼如下。
public class Resource {
private String name;
private int count;
public Resource(String name) {
this.name = name;
}
public void staticResource() {
synchronized (this) {
System.out.println("static resource");
count++;
}
}
public void saveResource(Resource resource) {
Resource lock = this.hashCode() > resource.hashCode() ? this : resource;
synchronized (lock) {
System.out.println("save resource:" + Thread.currentThread().getName());
resource.staticResource();
}
}
}
5.4 經(jīng)典的哲學家就餐問題
如圖所示:
- 有 5 個哲學家圍坐在一張圓桌旁。
- 每個哲學家都有一個吃飯和思考的狀態(tài)。
- 圓桌上放著 5 根筷子(與哲學家數(shù)量相同)。
- 哲學家必須同時拿起兩根筷子(左手和右手各一根)才能吃飯,吃完后放下筷子繼續(xù)思考。

問題描述:如果每個哲學家都拿起左邊的筷子并等待右邊的筷子,導致所有人相互等待,陷入死鎖。
- 編號為 0 的哲學家拿到編號為 0 的筷子,并等待編號為 1 的筷子。
- 編號為 1 的哲學家拿到編號為 1 的筷子,并等待編號為 2 的筷子。
- 編號為 2 的哲學家拿到編號為 2 的筷子,并等待編號為 3 的筷子。
- 編號為 3 的哲學家拿到編號為 3 的筷子,并等待編號為 4 的筷子。
- 編號為 4 的哲學家拿到編號為 4 的筷子,并等待編號為 0 的筷子。
哲學家就餐問題(死鎖):
public class DiningPhilosophers {
public static class Philosopher implements Runnable {
private Object leftChopstick;
private Object rightChopstick;
public Philosopher(Object leftChopstick, Object rightChopstick) {
this.leftChopstick = leftChopstick;
this.rightChopstick = rightChopstick;
}
@Override
public void run() {
while (true) {
//思考
try {
doAction("Thinking");
//吃飯
//拿起左邊筷子,拿起右邊筷子 放下右邊筷子 放下左邊筷子
synchronized (leftChopstick) {
doAction("Picked up left chopstick");
synchronized (rightChopstick) {
doAction("Picked up right chopstick -eating");
doAction("Put down right chopstick");
}
doAction("Put down left chopstick");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void doAction(String action) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " " + action);
Thread.sleep((long) Math.random() * 10);
}
}
public static void main(String[] args) {
Philosopher[] philosophers = new Philosopher[5];
Object[] chopsticks = new Object[philosophers.length];
for (int i = 0; i < chopsticks.length; i++) {
chopsticks[i] = new Object();
}
for (int i = 0; i < philosophers.length; i++) {
Object leftChopstick = chopsticks[i];
Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];
philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
new Thread(philosophers[i], "哲學家" + (i + 1) + "號").start();
}
}
}
解決的方式有很多,這里我們通過改變一個哲學家拿筷子的順序,解決死鎖問題。
哲學家就餐的換手方案:
public class DiningPhilosophersFix {
public static class Philosopher implements Runnable {
private Object leftChopstick;
private Object rightChopstick;
public Philosopher(Object leftChopstick, Object rightChopstick) {
this.leftChopstick = leftChopstick;
this.rightChopstick = rightChopstick;
}
@Override
public void run() {
while (true) {
//思考
try {
doAction("Thinking");
//吃飯
//拿起左邊筷子,拿起右邊筷子 放下右邊筷子 放下左邊筷子
synchronized (leftChopstick) {
doAction("Picked up left chopstick");
synchronized (rightChopstick) {
doAction("Picked up right chopstick -eating");
doAction("Put down right chopstick");
}
doAction("Put down left chopstick");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void doAction(String action) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " " + action);
Thread.sleep((long) Math.random() * 10);
}
}
public static void main(String[] args) {
Philosopher[] philosophers = new Philosopher[5];
Object[] chopsticks = new Object[philosophers.length];
for (int i = 0; i < chopsticks.length; i++) {
chopsticks[i] = new Object();
}
for (int i = 0; i < philosophers.length; i++) {
Object leftChopstick = chopsticks[i];
Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];
if (i == philosophers.length - 1) {
philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
} else {
philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
}
new Thread(philosophers[i], "哲學家" + (i + 1) + "號").start();
}
}
}
6. 實際工程中如何有效避免死鎖
- 設置超時時間:
Lock的tryLock(long timeout, TimeUnit unit);synchronized不具備嘗試鎖的能力。
- 使用最小化鎖:減少鎖的數(shù)量和作用范圍,能顯著降低死鎖發(fā)生的概率。
- 避免嵌套鎖:盡量避免線程在持有一個鎖時嘗試獲取另一個鎖。
- 使用高級并發(fā)工具:
Semaphore、CountDownLatch、ReadWriteLock。
7. 其他活性故障
死鎖是最常見的活躍性問題,除了死鎖之外,還有一些類似的問題,會導致程序無法順利執(zhí)行,統(tǒng)稱為活躍性問題。
7.1 活鎖
什么是活鎖:線程處于一種“忙碌但無效”的狀態(tài),始終無法完成任務。(俗稱內(nèi)耗)
特點:
- 程序一直在運行,但是一直在做沒有意義的工作。
活鎖代碼示例:
public class LiveLock {
static class Spoon {
private Diner owner; //就餐者
public synchronized void use() {
System.out.printf("%s has eaten!", owner.name);
}
public Spoon(Diner owner) {
this.owner = owner;
}
public Diner getOwner() {
return owner;
}
public void setOwner(Diner owner) {
this.owner = owner;
}
}
static class Diner {
private String name;
private boolean isHungry;
public Diner(String name) {
this.name = name;
isHungry = true;
}
public void eatWith(Spoon spoon, Diner spouse) {
while (isHungry) { //只有餓的情況下才能進來
//問題在此處:一直再謙讓
if (spouse.isHungry) {
System.out.println(name + ": 親愛的" + spouse.name + "你先吃吧");
spoon.setOwner(spouse);
continue;
}
spoon.use();
isHungry = false;
System.out.println(name + ": 我吃好了");
spoon.setOwner(spouse);
}
}
}
public static void main(String[] args) {
Diner husband = new Diner("牛郎");
Diner wife = new Diner("織女");
Spoon spoon = new Spoon(husband);
new Thread(new Runnable() {
@Override
public void run() {
husband.eatWith(spoon, wife);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
wife.eatWith(spoon, husband);
}
}).start();
}
}
打印結(jié)果:
牛郎: 親愛的織女你先吃吧 織女: 親愛的牛郎你先吃吧 牛郎: 親愛的織女你先吃吧 織女: 親愛的牛郎你先吃吧 牛郎: 親愛的織女你先吃吧 織女: 親愛的牛郎你先吃吧 牛郎: 親愛的織女你先吃吧 織女: 親愛的牛郎你先吃吧 牛郎: 親愛的織女你先吃吧 織女: 親愛的牛郎你先吃吧 ...
解決:以太網(wǎng)的指數(shù)退避算法,加入隨機因素。
public class LiveLockFix {
static class Spoon {
private Diner owner; //就餐者
public synchronized void use() {
System.out.printf("%s has eaten!", owner.name);
}
public Spoon(Diner owner) {
this.owner = owner;
}
public Diner getOwner() {
return owner;
}
public void setOwner(Diner owner) {
this.owner = owner;
}
}
static class Diner {
private String name;
private boolean isHungry;
public Diner(String name) {
this.name = name;
isHungry = true;
}
public void eatWith(Spoon spoon, Diner spouse) {
while (isHungry) { //只有餓的情況下才能進來
Random random = new Random();
//問題在此處:一直再謙讓
if (spouse.isHungry && random.nextInt(10) < 9) {
System.out.println(name + ": 親愛的" + spouse.name + "你先吃吧");
spoon.setOwner(spouse);
continue;
}
spoon.use();
isHungry = false;
System.out.println(name + ": 我吃好了");
spoon.setOwner(spouse);
}
}
}
public static void main(String[] args) {
Diner husband = new Diner("牛郎");
Diner wife = new Diner("織女");
Spoon spoon = new Spoon(husband);
new Thread(new Runnable() {
@Override
public void run() {
husband.eatWith(spoon, wife);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
wife.eatWith(spoon, husband);
}
}).start();
}
}
活鎖的解決方法:
- 增加隨機性:通過引入隨機的等待時間(如使用隨機退避算法),避免線程/進程按照相同的模式重復操作。
- 設置重試次數(shù)或超時:為線程的嘗試次數(shù)或時間限制設置一個閾值。如果超過限制,則采用其他策略,如強制退出或降級處理。
7.2 饑餓
線程饑餓問題其實指的公平性問題。是指某個線程因無法獲取所需資源而無法執(zhí)行,一直處于等待狀態(tài)的情況。
饑餓代碼示例:
public class StarvationExample {
private static final Object lock = new Object();
public static void main(String[] args) {
Thread highPriorityThread = new Thread(() -> {
synchronized (lock) {
while (true) {
System.out.println("High priority thread is running...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread lowPriorityThread = new Thread(() -> {
synchronized (lock) {
while (true) {
System.out.println("Low priority thread is running...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
highPriorityThread.setPriority(Thread.MAX_PRIORITY);
lowPriorityThread.setPriority(Thread.MIN_PRIORITY);
highPriorityThread.start();
lowPriorityThread.start();
}
}
問題解析:
- 上述代碼中,高優(yōu)先級線程(highPriorityThread)由于持有 lock 鎖資源,它可能會導致低優(yōu)先級線程(lowPriorityThread)一直無法執(zhí)行,從而出現(xiàn)線程饑餓的現(xiàn)象。
線程饑餓原因:
- 資源分配不均: 如果一個線程的優(yōu)先級一直較低,而系統(tǒng)的調(diào)度策略總是優(yōu)先執(zhí)行高優(yōu)先級的線程,那么低優(yōu)先級線程就可能一直得不到執(zhí)行的機會,從而發(fā)生饑餓。
- 線程被無限阻塞:當獲得鎖的線程需要執(zhí)行無限時間長的操作時(比如 IO 或者無限循環(huán)),那么后面的線程將會被無限阻塞,導致被餓死。
饑餓的解決方法:
- 設置合適的線程優(yōu)先級
- 使用公平性調(diào)度算法
8. 總結(jié)
- 死鎖
- 特點:兩個或多個線程(進程)相互等待對方釋放資源,導致所有線程都無法繼續(xù)執(zhí)行。
- 解決方法:避免一個線程持有多個資源的情況,或使用超時機制,如果一個線程在一定時間內(nèi)沒能獲得鎖,就放棄等待。
- 活鎖
- 特點:線程仍然在運行,但由于不斷地響應對方,始終沒有實際進展。
- 解決方法:為避免活鎖,可以設置超時機制,或者使用協(xié)調(diào)機制來避免線程之間過度的反應。
- 饑餓
- 特點:線程無法獲得執(zhí)行機會,但其他線程仍然在運行,造成某些線程得不到資源。
- 解決方法:使用公平鎖或合理的優(yōu)先級策略,確保每個線程都有機會執(zhí)行,不會被長時間忽略。
以上就是Java程序死鎖問題定位與解決方法的詳細內(nèi)容,更多關于Java程序死鎖的資料請關注腳本之家其它相關文章!
相關文章
java多線程實現(xiàn)同步鎖賣票實戰(zhàn)項目
本文主要介紹了java多線程實現(xiàn)同步鎖賣票實戰(zhàn)項目,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-01-01
java操作mongodb之多表聯(lián)查的實現(xiàn)($lookup)
這篇文章主要介紹了java操作mongodb之多表聯(lián)查的實現(xiàn)($lookup),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-03-03

