歡迎來到《並發王者課》,本文是該系列文章中的第13篇。
在上篇文章中,我們介紹了避免死鎖的幾種策略。雖然死鎖臭名昭著,然而在並發編程中,除了死鎖之外,還有一些同樣重要的線程活躍性問題值得關注。它們的知名度不高,但破壞性極強,本文將介紹的正是其中的線程飢餓和活鎖問題。
一、飢餓的產生
所謂線程 飢餓(Starvation) 指的是在多線程的資源競爭中,存在貪婪的線程一直鎖定資源不釋放,其他的線程則始終處於等待狀態,然而這個等待是沒有結果的,它們會被活活地餓死。
獨占者的貪婪是飢餓產生的原因之一,概括來說,飢餓一般由下面三種原因導致:
(1)線程被無限阻塞
當獲得鎖的線程需要執行無限時間長的操作時(比如IO或者無限循環),那么后面的線程將會被無限阻塞,導致被餓死。
(2) 線程優先級降低沒有獲得CPU時間
當多個競爭的線程被設置優先級之后,優先級越高,線程被給予的CPU時間越多。在某些極端情況下,低優先級的線程可能永遠無法被授予充足的CPU時間,從而導致被餓死。
(3) 線程永遠在等待資源
在青銅系列文章中,我們說過notify
在發送通知時,是無法喚醒指定線程的。當多個線程都處於wait
時,那么部分線程可能始終無法被通知到,以至於挨餓。
二、飢餓與公平
為了直觀體驗線程的飢餓,我們創建了下面的代碼。
創建哪吒、蘭陵王等四個英雄玩家,他們以競爭的方式打野,殺死野怪可以獲得經濟收益。
public class StarvationExample {
public static void main(String[] args) {
final WildMonster wildMonster = new WildMonster();
String[] players = {
"哪吒",
"蘭陵王",
"鎧",
"典韋"
};
for (String player: players) {
Thread playerThread = new Thread(new Runnable() {
public void run() {
wildMonster.killWildMonster();
}
});
playerThread.setName(player);
playerThread.start();
}
}
}
public class WildMonster {
public synchronized void killWildMonster() {
while (true) {
String playerName = Thread.currentThread().getName();
System.out.println(playerName + "斬獲野怪!");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("打野中斷");
}
}
}
}
運行結果如下:
哪吒斬獲野怪!
哪吒斬獲野怪!
哪吒斬獲野怪!
哪吒斬獲野怪!
哪吒斬獲野怪!
哪吒斬獲野怪!
哪吒斬獲野怪!
哪吒斬獲野怪!
哪吒斬獲野怪!
哪吒斬獲野怪!
哪吒斬獲野怪!
Process finished with exit code 130 (interrupted by signal 2: SIGINT)
從結果中可以看到,在幾個線程的運行中,始終只有哪吒可以斬獲野怪,其他英雄束手無策等着被餓死。為什么會發生這樣的事?
仔細看WildMonster類中的代碼,問題出在killWildMonster
同步方法中。一旦某個英雄進入該方法后,將一直持有對象鎖,其他線程被阻塞而無法再進入。
當然,解決的方法也很簡單,只要打破獨占即可。比如,我們在下面的代碼中把Thread.sleep
改成wait
,那么問題將迎刃而解。
public static class WildMonster {
public synchronized void killWildMonster() {
while (true) {
String playerName = Thread.currentThread().getName();
System.out.println(playerName + "斬獲野怪!");
try {
wait(500);
} catch (InterruptedException e) {
System.out.println("打野中斷");
}
}
}
}
運行結果如下:
哪吒斬獲野怪!
鎧斬獲野怪!
蘭陵王斬獲野怪!
典韋斬獲野怪!
蘭陵王斬獲野怪!
典韋斬獲野怪!
Process finished with exit code 130 (interrupted by signal 2: SIGINT)
從結果中可以看到,四個英雄都獲得了打野的機會,在一定程度上實現了公平。(備注:wait
會釋放鎖,但sleep
不會,對此不理解的可以查看青銅系列文章。)
如何讓線程之間公平競爭,是線程問題中的重要話題。雖然我們無法保證百分百的公平,但我們仍然要通過設計一定的數據結構和使用相應的工具類來增加線程之間的公平性。
關於線程之間的公平性,在本文中重要的是理解它的存在和重要性,關於如何優雅地解決,我們會在后續的文章中介紹相關的並發工具類。
三、活鎖的麻煩
相對於死鎖,你可能對活鎖沒有那么熟悉。然而,活鎖所造成的負面影響並不亞於死鎖。在結果上,活鎖和死鎖都是災難性的,都將會造成應用程序無法提供正常的服務能力。
所謂活鎖(LiveLock),指的是兩個線程都忙於響應對方的請求,但卻不干自己的事。它們不斷地重復特定的代碼,卻一事無成。
不同於死鎖,活鎖並不會造成線程進入阻塞狀態,但它們會原地打轉,所以在影響上和死鎖相似,程序會進入無線死循環,無法繼續進行。
如果你無法直觀理解活鎖是什么,相信你在走路時一定遇到過下面這種情況。兩人相向而行,出於禮貌兩人互相讓行,讓來讓去,結果兩人仍然無法通行。活鎖,也是這個意思。
小結
以上就是關於線程飢餓與活鎖的全部內容。在本文中,我們介紹了線程產生飢餓的原因。對待線程飢餓,沒有百分百的方案,但可以盡可能地實現公平競爭。我們沒有在本文列舉線程公平性的一些工具類,因為我認為對問題的理解要比解決方案更重要。如果沒有對問題的理解,方案在落地時也會出現知其然而不知其所以然的情況。另外,雖然活鎖並不像死鎖那樣知名度,但是對活鎖的恰當理解仍然非常必要,它是並發知識體系中的一部分。
正文到此結束,恭喜你又上了一顆星✨
夫子的試煉
- 編寫代碼設置不同線程的優先級,體驗線程飢餓並給出解決方案。
延伸閱讀與參考資料
關於作者
關注公眾號【庸人技術笑談】,獲取及時文章更新。記錄平凡人的技術故事,分享有品質(盡量)的技術文章,偶爾也聊聊生活和理想。不販賣焦慮,不做標題黨。
如果本文對你有幫助,歡迎點贊、關注、監督,我們一起從青銅到王者。