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