線程上下文切換與死鎖
1. 前言
本節(jié)內(nèi)容主要是對(duì)死鎖進(jìn)行深入的講解,具體內(nèi)容點(diǎn)如下:
- 理解線程的上下文切換,這是本節(jié)的輔助基礎(chǔ)內(nèi)容,從概念層面進(jìn)行理解即可;
- 了解什么是線程死鎖,在并發(fā)編程中,線程死鎖是一個(gè)致命的錯(cuò)誤,死鎖的概念是本節(jié)的重點(diǎn)之一;
- 了解線程死鎖的必備 4 要素,這是避免死鎖的前提,了解死鎖的必備要素,才能找到避免死鎖的方式;
- 掌握死鎖的實(shí)現(xiàn),通過代碼實(shí)例,進(jìn)行死鎖的實(shí)現(xiàn),深入體會(huì)什么是死鎖,這是本節(jié)的重難點(diǎn)之一;
- 掌握如何避免線程死鎖,我們能夠?qū)崿F(xiàn)死鎖,也可以避免死鎖,這是本節(jié)內(nèi)容的核心。
2. 理解線程的上下文切換
概述:在多線程編程中,線程個(gè)數(shù)一般都大于 CPU 個(gè)數(shù),而每個(gè) CPU 同一時(shí)-刻只能被一個(gè)線程使用,為了讓用戶感覺多個(gè)線程是在同時(shí)執(zhí)行的, CPU 資源的分配采用了時(shí)間片輪轉(zhuǎn)的策略,也就是給每個(gè)線程分配一個(gè)時(shí)間片,線程在時(shí)間片內(nèi)占用 CPU 執(zhí)行任務(wù)。
定義:當(dāng)前線程使用完時(shí)間片后,就會(huì)處于就緒狀態(tài)并讓出 CPU,讓其他線程占用,這就是上下文切換,從當(dāng)前線程的上下文切換到了其他線程。
問題點(diǎn)解析:那么就有一個(gè)問題,讓出 CPU 的線程等下次輪到自己占有 CPU 時(shí)如何知道自己之前運(yùn)行到哪里了?所以在切換線程上下文時(shí)需要保存當(dāng)前線程的執(zhí)行現(xiàn)場(chǎng), 當(dāng)再次執(zhí)行時(shí)根據(jù)保存的執(zhí)行現(xiàn)場(chǎng)信息恢復(fù)執(zhí)行現(xiàn)場(chǎng)。
線程上下文切換時(shí)機(jī): 當(dāng)前線程的 CPU 時(shí)間片使用完或者是當(dāng)前線程被其他線程中斷時(shí),當(dāng)前線程就會(huì)釋放執(zhí)行權(quán)。那么此時(shí)執(zhí)行權(quán)就會(huì)被切換給其他的線程進(jìn)行任務(wù)的執(zhí)行,一個(gè)線程釋放,另外一個(gè)線程獲取,就是我們所說的上下文切換時(shí)機(jī)。
3. 什么是線程死鎖
定義:死鎖是指兩個(gè)或兩個(gè)以上的線程在執(zhí)行過程中,因爭(zhēng)奪資源而造成的互相等待的現(xiàn)象,在無外力作用的情況下,這些線程會(huì)一直相互等待而無法繼續(xù)運(yùn)行下去。
如上圖所示死鎖狀態(tài),線程 A 己經(jīng)持有了資源 2,它同時(shí)還想申請(qǐng)資源 1,可是此時(shí)線程 B 已經(jīng)持有了資源 1 ,線程 A 只能等待。
反觀線程 B 持有了資源 1 ,它同時(shí)還想申請(qǐng)資源 2,但是資源 2 已經(jīng)被線程 A 持有,線程 B 只能等待。所以線程 A 和線程 B 就因?yàn)橄嗷サ却龑?duì)方已經(jīng)持有的資源,而進(jìn)入了死鎖狀態(tài)。
4. 線程死鎖的必備要素
- 互斥條件:進(jìn)程要求對(duì)所分配的資源進(jìn)行排他性控制,即在一段時(shí)間內(nèi)某資源僅為一個(gè)進(jìn)程所占有。此時(shí)若有其他進(jìn)程請(qǐng)求該資源,則請(qǐng)求進(jìn)程只能等待;
- 不可剝奪條件:進(jìn)程所獲得的資源在未使用完畢之前,不能被其他進(jìn)程強(qiáng)行奪走,即只能由獲得該資源的進(jìn)程自己來釋放(只能是主動(dòng)釋放,如 yield 釋放 CPU 執(zhí)行權(quán));
- 請(qǐng)求與保持條件:進(jìn)程已經(jīng)保持了至少一個(gè)資源,但又提出了新的資源請(qǐng)求,而該資源已被其他進(jìn)程占有,此時(shí)請(qǐng)求進(jìn)程被阻塞,但對(duì)自己已獲得的資源保持不放;
- 循環(huán)等待條件:指在發(fā)生死鎖時(shí),必然存在一個(gè)線程請(qǐng)求資源的環(huán)形鏈,即線程集合 {T0,T1,T2,…Tn}中的 T0 正在等待一個(gè) T1 占用的資源,T1 正在等待 T2 占用的資源,以此類推,Tn 正在等待己被 T0 占用的資源。
如下圖所示:
5. 死鎖的實(shí)現(xiàn)
為了更好的了解死鎖是如何產(chǎn)生的,我們首先來設(shè)計(jì)一個(gè)死鎖爭(zhēng)奪資源的場(chǎng)景。
場(chǎng)景設(shè)計(jì):
- 創(chuàng)建 2 個(gè)線程,線程名分別為 threadA 和 threadB;
- 創(chuàng)建兩個(gè)資源, 使用 new Object () 創(chuàng)建即可,分別命名為 resourceA 和 resourceB;
- threadA 持有 resourceA 并申請(qǐng)資源 resourceB;
- threadB 持有 resourceB 并申請(qǐng)資源 resourceA ;
- 為了確保發(fā)生死鎖現(xiàn)象,請(qǐng)使用 sleep 方法創(chuàng)造該場(chǎng)景;
- 執(zhí)行代碼,看是否會(huì)發(fā)生死鎖。
期望結(jié)果:發(fā)生死鎖,線程 threadA 和 threadB 互相等待。
Tips:此處的實(shí)驗(yàn)會(huì)使用到關(guān)鍵字 synchronized,后續(xù)小節(jié)還會(huì)對(duì)關(guān)鍵字 synchronized 單獨(dú)進(jìn)行深入講解,此處對(duì) synchronized 的使用僅僅為初級(jí)使用,有 JavaSE 基礎(chǔ)即可。
實(shí)例:
public class DemoTest{
private static Object resourceA = new Object();//創(chuàng)建資源 resourceA
private static Object resourceB = new Object();//創(chuàng)建資源 resourceB
public static void main(String[] args) throws InterruptedException {
//創(chuàng)建線程 threadA
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA) {
System.out.println(Thread.currentThread().getName() + "獲取 resourceA。");
try {
Thread.sleep(1000); // sleep 1000 毫秒,確保此時(shí) resourceB 已經(jīng)進(jìn)入run 方法的同步模塊
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "開始申請(qǐng) resourceB。");
synchronized (resourceB) {
System.out.println (Thread.currentThread().getName() + "獲取 resourceB。");
}
}
}
});
threadA.setName("threadA");
//創(chuàng)建線程 threadB
Thread threadB = new Thread(new Runnable() { //創(chuàng)建線程 1
@Override
public void run() {
synchronized (resourceB) {
System.out.println(Thread.currentThread().getName() + "獲取 resourceB。");
try {
Thread.sleep(1000); // sleep 1000 毫秒,確保此時(shí) resourceA 已經(jīng)進(jìn)入run 方法的同步模塊
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "開始申請(qǐng) resourceA。");
synchronized (resourceA) {
System.out.println (Thread.currentThread().getName() + "獲取 resourceA。");
}
}
}
});
threadB.setName("threadB");
threadA. start();
threadB. start();
}
}
代碼講解:
- 從代碼中來看,我們首先創(chuàng)建了兩個(gè)資源 resourceA 和 resourceB;
- 然后創(chuàng)建了兩條線程 threadA 和 threadB。threadA 首先獲取了 resourceA ,獲取的方式是代碼 synchronized (resourceA) ,然后沉睡 1000 毫秒;
- 在 threadA 沉睡過程中, threadB 獲取了 resourceB,然后使自己沉睡 1000 毫秒;
- 當(dāng)兩個(gè)線程都蘇醒時(shí),此時(shí)可以確定 threadA 獲取了 resourceA,threadB 獲取了 resourceB,這就達(dá)到了我們做的第一步,線程分別持有自己的資源;
- 那么第二步就是開始申請(qǐng)資源,threadA 申請(qǐng)資源 resourceB,threadB 申請(qǐng)資源 resourceA 無奈 resourceA 和 resourceB 都被各自線程持有,兩個(gè)線程均無法申請(qǐng)成功,最終達(dá)成死鎖狀態(tài)。
執(zhí)行結(jié)果驗(yàn)證:
threadA 獲取 resourceA。
threadB 獲取 resourceB。
threadA 開始申請(qǐng) resourceB。
threadB 開始申請(qǐng) resourceA。
看下驗(yàn)證結(jié)果,發(fā)現(xiàn)已經(jīng)出現(xiàn)死鎖,threadA 申請(qǐng) resourceB,threadB 申請(qǐng) resourceA,但均無法申請(qǐng)成功,死鎖得以實(shí)驗(yàn)成功。
6. 如何避免線程死鎖
要想避免死鎖,只需要破壞掉至少一個(gè)構(gòu)造死鎖的必要條件即可,學(xué)過操作系統(tǒng)的讀者應(yīng)該都知道,目前只有請(qǐng)求并持有和環(huán)路等待條件是可以被破壞的。
造成死鎖的原因其實(shí)和申請(qǐng)資源的順序有很大關(guān)系,使用資源申請(qǐng)的有序性原則就可避免死鎖。
我們依然以第 5 個(gè)知識(shí)點(diǎn)進(jìn)行講解,那么實(shí)驗(yàn)的需求和場(chǎng)景不變,我們僅僅對(duì)之前的 threadB 的代碼做如下修改,以避免死鎖。
代碼修改:
Thread threadB = new Thread(new Runnable() { //創(chuàng)建線程 1
@Override
public void run() {
synchronized (resourceA) { //修改點(diǎn) 1
System.out.println(Thread.currentThread().getName() + "獲取 resourceB。");//修改點(diǎn) 3
try {
Thread.sleep(1000); // sleep 1000 毫秒,確保此時(shí) resourceA 已經(jīng)進(jìn)入run 方法的同步模塊
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "開始申請(qǐng) resourceA。");//修改點(diǎn) 4
synchronized (resourceB) { //修改點(diǎn) 2
System.out.println (Thread.currentThread().getName() + "獲取 resourceA。"); //修改點(diǎn) 5
}
}
}
});
請(qǐng)看如上代碼示例,有 5 個(gè)修改點(diǎn):
- 修改點(diǎn) 1 :將 resourceB 修改成 resourceA;
- 修改點(diǎn) 2 :將 resourceA 修改成 resourceB;
- 修改點(diǎn) 3 :將 resourceB 修改成 resourceA;
- 修改點(diǎn) 4 :將 resourceA 修改成 resourceB;
- 修改點(diǎn) 5 :將 resourceA 修改成 resourceB。
請(qǐng)讀者按指示修改代碼,并從新運(yùn)行驗(yàn)證。
修改后代碼講解:
- 從代碼中來看,我們首先創(chuàng)建了兩個(gè)資源 resourceA 和 resourceB;
- 然后創(chuàng)建了兩條線程 threadA 和 threadB。threadA 首先獲取了 resourceA ,獲取的方式是代碼 synchronized (resourceA) ,然后沉睡 1000 毫秒;
- 在 threadA 沉睡過程中, threadB 想要獲取 resourceA ,但是 resourceA 目前正被沉睡的 threadA 持有,所以 threadB 等待 threadA 釋放 resourceA;
- 1000 毫秒后,threadA 蘇醒了,釋放了 resourceA ,此時(shí)等待的 threadB 獲取到了 resourceA,然后 threadB 使自己沉睡 1000 毫秒;
- threadB 沉睡過程中,threadA 申請(qǐng) resourceB 成功,繼續(xù)執(zhí)行成功后,釋放 resourceB;
- 1000 毫秒后,threadB 蘇醒了,繼續(xù)執(zhí)行獲取 resourceB ,執(zhí)行成功。
執(zhí)行結(jié)果驗(yàn)證:
threadA 獲取 resourceA。
threadA 開始申請(qǐng) resourceB。
threadA 獲取 resourceB。
threadB 獲取 resourceA。
threadB 開始申請(qǐng) resourceB。
threadB 獲取 resourceB。
我們發(fā)現(xiàn) threadA 和 threadB 按照相同的順序?qū)?resourceA 和 resourceB 依次進(jìn)行訪問,避免了互相交叉持有等待的狀態(tài),避免了死鎖的發(fā)生。
7. 小結(jié)
死鎖是并發(fā)編程中最致命的問題,如何避免死鎖,是并發(fā)編程中恒久不變的問題。
掌握死鎖的實(shí)現(xiàn)以及如果避免死鎖的發(fā)生,是本節(jié)內(nèi)容的重中之重。