Java并發(fā)編程:線程活躍性問題:死鎖、活鎖與饑餓
活躍性問題意味著程序永遠(yuǎn)無法得到運行的最終結(jié)果。與之前提到的線程安全問題導(dǎo)致的程序錯誤相比,活躍性問題的后果可能更嚴(yán)重。例如,若發(fā)生死鎖,程序會完全卡死無法運行。
最典型的三種活躍性問題是死鎖(Deadlock)、活鎖(Livelock)和饑餓(Starvation)。下面逐一介紹。
1. 死鎖(Deadlock)
最常見的活躍性問題是死鎖。當(dāng)兩個線程互相等待對方持有的資源,且都不釋放自己已持有的資源時,就會導(dǎo)致永久阻塞。
代碼示例:
public class DeadLock {
static Object lock1 = new Object();
static Object lock2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
try {
synchronized (lock1) {
Thread.sleep(500);
synchronized (lock2) {
System.out.println("Thread 1 成功執(zhí)行");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
synchronized (lock2) {
Thread.sleep(500);
synchronized (lock1) {
System.out.println("Thread 2 成功執(zhí)行");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
輸出結(jié)果:
Acquired lock1, trying to acquire lock2.
Acquired lock2, trying to acquire lock1.
啟動程序后會發(fā)現(xiàn),程序一直在運行,但永遠(yuǎn)無法輸出線程 1 和線程 2 的執(zhí)行結(jié)果,說明兩者都被卡住了。如果不強制終止進(jìn)程,它們將永遠(yuǎn)等待。
注意:后續(xù)章節(jié)會詳細(xì)講解
synchronized
關(guān)鍵字,目前只需知道它能確保同一時刻最多一個線程執(zhí)行代碼(需持有對應(yīng)鎖),以控制并發(fā)安全。
死鎖的必要條件
根據(jù)上述示例,可以分析死鎖發(fā)生的四個必要條件:
- 互斥條件:資源一次只能被一個進(jìn)程或線程使用。例如,鎖被某個線程持有后,其他線程無法獲取,直到釋放。
- 請求與保持條件:線程在持有第一個鎖的同時請求第二個鎖。例如,線程 1 持有鎖 A 后嘗試獲取鎖 B,且不釋放鎖 A。
- 不可剝奪條件:鎖不會被外部強制剝奪。即沒有外界干預(yù)來終止死鎖。
- 循環(huán)等待條件:多個線程形成環(huán)形等待鏈。例如,線程 A 等線程 B 釋放資源,線程 B 等線程 A 釋放資源;或多個線程形成 A→B→C→A 的循環(huán)等待鏈。
??以上四個條件缺一不可!只要破壞任意一個條件,即可避免死鎖!
如何預(yù)防死鎖
如果線程一次只能獲取一個鎖,則不會發(fā)生死鎖。雖然不太實用,但這是最徹底的解決方案。
以下是兩種常用預(yù)防方法:
- 按固定順序獲取鎖
如果必須獲取多個鎖,設(shè)計時需要確保所有線程按相同順序獲取鎖。例如修改上述代碼:
// 線程 1 和線程 2 均按 lock1 → lock2 順序獲取
Thread1--> 獲取lock1--> 獲取lock2--> 執(zhí)行成功;
Thread2--> 獲取lock1--> 獲取lock2--> 執(zhí)行成功;
- 超時放棄
使用synchronized
內(nèi)置鎖時,線程會無限等待。而Lock
接口的tryLock(long time, TimeUnit unit)
方法允許設(shè)置等待時間。若超時未獲鎖,線程可主動釋放已持有的鎖,從而避免死鎖。
2. 活鎖(Livelock)
什么是活鎖
活鎖是第二種活躍性問題。與死鎖類似,程序無法得到最終結(jié)果,但線程并非完全阻塞,而是不斷嘗試執(zhí)行卻無法推進(jìn)。
例如:兩人迎面相遇,互相讓路,結(jié)果你往右我往左,再次相撞,最終誰也無法通過。
代碼示例:
public class Livelock {
private Lock lock1 = new ReentrantLock(true);
private Lock lock2 = new ReentrantLock(true);
public static void main(String[] args) {
Livelock livelock = new Livelock();
new Thread(livelock::operation1, "T1").start();
new Thread(livelock::operation2, "T2").start();
}
public void operation1() {
while (true) {
lock1.tryLock();
System.out.println("獲取 lock1,嘗試獲取 lock2");
sleep(50); // 模擬業(yè)務(wù)耗時
if (lock2.tryLock()) {
System.out.println("獲取 lock2");
} else {
System.out.println("無法獲取 lock2,釋放 lock1");
lock1.unlock();
continue;
}
System.out.println("執(zhí)行 operation1");
break;
}
lock2.unlock();
lock1.unlock();
}
public void operation2() {
while (true) {
lock2.tryLock();
System.out.println("獲取 lock2,嘗試獲取 lock1");
sleep(50);
if (lock1.tryLock()) {
System.out.println("獲取 lock1");
} else {
System.out.println("無法獲取 lock1,釋放 lock2");
lock2.unlock();
continue;
}
System.out.println("執(zhí)行 operation2");
break;
}
lock1.unlock();
lock2.unlock();
}
private void sleep(long sleepTime) {
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
輸出結(jié)果:
獲取lock1,嘗試獲取lock2
獲取lock2,嘗試獲取lock1
無法獲取lock2,釋放lock1
獲取lock2,嘗試獲取lock1
無法獲取lock1,釋放lock2
...(循環(huán))
從日志可見,兩個線程不斷獲取和釋放鎖,但都無法完成操作。
注意:由于線程調(diào)度,此示例可能在運行一段時間后自動解除活鎖,但不影響理解其原理。
如何預(yù)防活鎖
活鎖的根源在于線程同時釋放鎖并重試。解決方法是為鎖獲取設(shè)置隨機等待時間,打破同步釋放的節(jié)奏:
修改代碼:
// 在 sleep 方法中增加隨機等待時間
private void sleep(long sleepTime) {
try {
Thread.sleep(sleepTime + (long)(Math.random() * 100));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
修改后運行結(jié)果:
獲取lock1,嘗試獲取lock2
獲取lock2,嘗試獲取lock1
無法獲取lock1,釋放lock2
獲取lock2
執(zhí)行operation1
獲取lock2,嘗試獲取lock1
獲取lock1
執(zhí)行operation2
此時活鎖問題基本消失。
典型場景:消息隊列中某個錯誤消息反復(fù)重試,導(dǎo)致線程忙但無結(jié)果。解決方法:
- 將錯誤消息移至隊列尾部延遲處理;
- 限制重試次數(shù),超過后丟棄或特殊處理。
3. 饑餓(Starvation)
什么是饑餓
饑餓指線程長期無法獲取資源(如 CPU 時間),導(dǎo)致無法運行。常見場景:
- 線程優(yōu)先級過低,長期得不到調(diào)度;
- 某線程持有鎖且不釋放(如無限循環(huán)),其他線程長期等待。
饑餓的影響
導(dǎo)致程序響應(yīng)性差。例如,瀏覽器前端線程因后臺線程占用 CPU 無法響應(yīng)操作。
如何預(yù)防饑餓
- 確保邏輯正確,及時釋放鎖;
- 合理設(shè)置線程優(yōu)先級(或不設(shè)置優(yōu)先級)。