既然多線程能讓我們充分發(fā)揮多處理器優(yōu)勢,提升性能,那是不是啟用線程越多越好呢?在多線程編程過程中都會遇到哪些問題呢?通過本節(jié)的學(xué)習(xí),相信你就得到答案。
1. 線程安全問題
我們先看幾個多線程的代碼栗子
樣例 1: 火車票多窗口售票
假設(shè)火車站有 10 萬張火車票,有三個售票窗口,售完為止,最后輸出我們一共售賣了多少張火車票,我們通過多線程代碼來實現(xiàn)它。
public class TrainTest {
//剩余的火車票數(shù)量
public static Integer leftTicketTotal = 10000;
//售出的火車票數(shù)量
public static Integer selledTicketTotal = 0;
public static class TicketWindow implements Runnable {
@Override
public void run() {
while (leftTicketTotal > 0) {
selledTicketTotal++;
leftTicketTotal--;
}
}
}
public static void main(String[] args) throws InterruptedException {
//啟動窗口1售票線程
Thread thread1 = new Thread(new TicketWindow());
thread1.start();
//啟動窗口2售票線程
Thread thread2 = new Thread(new TicketWindow());
thread2.start();
//啟動窗口3售票線程
Thread thread3 = new Thread(new TicketWindow());
thread3.start();
//等待三個線程執(zhí)行完成
thread1.join();
thread2.join();
thread3.join();
//輸出最終火車票數(shù)量
System.out.println("售出火車票數(shù)量:" + selledTicketTotal + " 剩余火車票數(shù)量:" + leftTicketTotal);
}
}
運行后,我們得到的輸出結(jié)果卻是,售出火車票數(shù)與總數(shù)不一致。
售出火車票數(shù)量:9099 剩余火車票數(shù)量:-2
而且我們發(fā)現(xiàn)每次輸出的結(jié)果都不一樣。接下來我們分析下造成不一致的原因
我們看到代碼 selledTicketTotal++
(即 selledTicketTotal = selledTicketTotal + 1) , 實際上包括三個操作,讀取 selledTicketTotal 的值,進(jìn)行加 1 操作,寫入新的值;同理 leftTicketTotal--
也包括三個操作,讀取 leftTicketTotal 的值,進(jìn)行減 1 操作,寫入新的值。這三個操作組成的 selledTicketTotal++
和 selledTicketTotal--
都是非原子操作的,那什么是原子操作呢?
原子操作是指不可被分割的一系列操作。
對一個變量的非原子操作往往會產(chǎn)生非預(yù)期的結(jié)果,比如線程 A 和線程 B 都在執(zhí)行 selledTicketTotal++
,線程 A 讀到 selledTicketTotal=10, 由于非原子操作是可被分割的,此時線程 B 不會等待 A 操作完成執(zhí)行加 1 操作,而是同樣讀到了 selledTicketTotal=10,線程 A 和 B 以 10 做基數(shù)分別做加 1 操作,selledTicketTotal 最終結(jié)果為 11,而不是預(yù)期的 12,這就是非原子操作帶來數(shù)據(jù)不一致。
樣例 2: 水龍頭開關(guān)
假設(shè)我們向蓄水池中放水,一段時間后停止放水
public class WaterTapTest {
public static boolean tapOpen = true;
public static class WaterTapTask implements Runnable {
@Override
public void run() {
while (tapOpen) {
try {
System.out.println("水龍頭放水");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new WaterTapTask());
thread.start();
Thread.sleep(1000);
tapOpen = false;
System.out.println("水龍頭停止放水");
//一定概率下,會繼續(xù)輸出水龍頭放水
thread.join();
}
}
在我們將 tapOpen 設(shè)置為 false 后,水龍頭依然在放水,這就是我們要說的第二個問題,多線程下的可見性問題,即當(dāng)一個線程更新了一個變量,另一個線程并不能及時得到變量修改后的值。那什么是可見性呢?
可見性:當(dāng)一個線程修改了對象狀態(tài)后,其他線程能夠看到發(fā)生的狀態(tài)變化。
當(dāng)讀操作和寫操作在不同的線程中執(zhí)行時,我們無法確保執(zhí)行讀操作的線程能實時看到其他線程寫入的值。
以上兩個例子闡述了非原子和不可見帶來的問題,這兩類問題均屬于線程安全問題。線程安全的定義:
多個線程訪問某個類時,這個類始終表現(xiàn)出正確的行為,那么就稱這個類是線程安全的。
綜上所述,要保證線程安全需要滿足兩大條件
- 原子性:一系列操作,要么全部完成,要么全部不完成,不可被分割,不會結(jié)束在中間某個環(huán)節(jié)。
- 可見性:當(dāng)一個線程修改了對象狀態(tài)后,其他線程能夠看到發(fā)生的狀態(tài)變化。
保證原子性的手段有單線程、加鎖、CAS (后續(xù)章節(jié)我們會介紹到),保證可見性的手段是通過插入內(nèi)存屏障 (后續(xù)章節(jié)我們會介紹) 來解決。
2. 上下文切換
Java 中的線程與 CPU 單核執(zhí)行是一對一的,即單個處理器同一時間只能處理一個線程的執(zhí)行;而 CPU 是通過時間片算法來執(zhí)行任務(wù)的,不同的線程活躍狀態(tài)不同,CPU 會在多個線程間切換執(zhí)行,在切換時會保存上一個任務(wù)的狀態(tài),以便下次切換回這個任務(wù)時可以再加載到這個任務(wù)的狀態(tài),這種任務(wù)的保存到加載就是一次上下文切換。線程數(shù)越多,帶來的上下文切換越嚴(yán)重,上下文切換會帶來 CPU 系統(tǒng)態(tài)使用率占用,這就是為什么當(dāng)我們開啟大量線程,系統(tǒng)反而更慢的原因。
我們要減少上下文切換,有幾種手段:
- 減少鎖等待:鎖等待意味著,線程頻繁在活躍與等待狀態(tài)之間切換,增加上下文切換,鎖等待是由對同一份資源競爭激烈引起的,在一些場景我們可以用一些手段減輕鎖競爭,比如數(shù)據(jù)分片或者數(shù)據(jù)快照等方式。
- CAS 算法:利用 Compare and Swap, 即比較再交換可以避免加鎖。后續(xù)章節(jié)會介紹 CAS 算法。
- 使用合適的線程數(shù)或者協(xié)程:使用合適的線程數(shù)而不是越多越好,在 CPU 密集的系統(tǒng)中,比如我們傾向于啟動最多 2 倍處理器核心數(shù)量的線程;協(xié)程由于天然在單線程實現(xiàn)多任務(wù)的調(diào)度,所以協(xié)程實際上避免了上下文切換。
3. 活躍性問題 (死鎖、饑餓)
當(dāng)某些操作遲遲得不到執(zhí)行時,就被認(rèn)為是產(chǎn)生了活躍性問題,活躍性分為兩類,一類是死鎖,一類是饑餓。
死鎖是最常見的活躍性問題,除此之外還有饑餓、活鎖。當(dāng)線程由于無法訪它所需的資源而不能繼續(xù)執(zhí)行時,就發(fā)生了饑餓。
在多線程開發(fā)中,我們要避免線程安全問題,勢必要對共享的數(shù)據(jù)資源進(jìn)行加鎖,而加鎖處理不當(dāng)即會帶來死鎖。
我們以死鎖為例,看看死鎖是如何發(fā)生的:
我們看上面這張圖,線程 A 和線程 B 都擁有一份鎖,而線程 A 和線程 B 恰好同時去獲取對方擁有的那把鎖,導(dǎo)致兩個線程永遠(yuǎn)無法執(zhí)行,要避免死鎖有一個方法即獲取鎖的順序是固定的,比如只能先獲取鎖 X 再獲取鎖 Y,不允許出現(xiàn)相反的順序。
4. 總結(jié)
多線程能給我們帶來很多好處,比如充分利用多核處理能力,建模簡單,異步事件簡化處理。
但在多線程在運行過程中,會帶來三類問題,分別是線程安全性、上下文切換和活躍性問題,接下來章節(jié)的我們就從起因到解決再分析原理一起攻克這三座大山。
下圖是本小節(jié)的腦圖整理
參考資料
- 《Java 并發(fā)編程實戰(zhàn)》
- 維基百科