亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

線程上下文切換與死鎖

1. 前言

本節內容主要是對死鎖進行深入的講解,具體內容點如下:

  • 理解線程的上下文切換,這是本節的輔助基礎內容,從概念層面進行理解即可;
  • 了解什么是線程死鎖,在并發編程中,線程死鎖是一個致命的錯誤,死鎖的概念是本節的重點之一;
  • 了解線程死鎖的必備 4 要素,這是避免死鎖的前提,了解死鎖的必備要素,才能找到避免死鎖的方式;
  • 掌握死鎖的實現,通過代碼實例,進行死鎖的實現,深入體會什么是死鎖,這是本節的重難點之一;
  • 掌握如何避免線程死鎖,我們能夠實現死鎖,也可以避免死鎖,這是本節內容的核心。

2. 理解線程的上下文切換

概述:在多線程編程中,線程個數一般都大于 CPU 個數,而每個 CPU 同一時-刻只能被一個線程使用,為了讓用戶感覺多個線程是在同時執行的, CPU 資源的分配采用了時間片輪轉的策略,也就是給每個線程分配一個時間片,線程在時間片內占用 CPU 執行任務。
圖片描述
定義:當前線程使用完時間片后,就會處于就緒狀態并讓出 CPU,讓其他線程占用,這就是上下文切換,從當前線程的上下文切換到了其他線程。

問題點解析:那么就有一個問題,讓出 CPU 的線程等下次輪到自己占有 CPU 時如何知道自己之前運行到哪里了?所以在切換線程上下文時需要保存當前線程的執行現場, 當再次執行時根據保存的執行現場信息恢復執行現場。

線程上下文切換時機: 當前線程的 CPU 時間片使用完或者是當前線程被其他線程中斷時,當前線程就會釋放執行權。那么此時執行權就會被切換給其他的線程進行任務的執行,一個線程釋放,另外一個線程獲取,就是我們所說的上下文切換時機。

3. 什么是線程死鎖

定義:死鎖是指兩個或兩個以上的線程在執行過程中,因爭奪資源而造成的互相等待的現象,在無外力作用的情況下,這些線程會一直相互等待而無法繼續運行下去。
圖片描述
如上圖所示死鎖狀態,線程 A 己經持有了資源 2,它同時還想申請資源 1,可是此時線程 B 已經持有了資源 1 ,線程 A 只能等待。

反觀線程 B 持有了資源 1 ,它同時還想申請資源 2,但是資源 2 已經被線程 A 持有,線程 B 只能等待。所以線程 A 和線程 B 就因為相互等待對方已經持有的資源,而進入了死鎖狀態。

4. 線程死鎖的必備要素

  • 互斥條件:進程要求對所分配的資源進行排他性控制,即在一段時間內某資源僅為一個進程所占有。此時若有其他進程請求該資源,則請求進程只能等待;
  • 不可剝奪條件:進程所獲得的資源在未使用完畢之前,不能被其他進程強行奪走,即只能由獲得該資源的進程自己來釋放(只能是主動釋放,如 yield 釋放 CPU 執行權);
  • 請求與保持條件:進程已經保持了至少一個資源,但又提出了新的資源請求,而該資源已被其他進程占有,此時請求進程被阻塞,但對自己已獲得的資源保持不放;
  • 循環等待條件:指在發生死鎖時,必然存在一個線程請求資源的環形鏈,即線程集合 {T0,T1,T2,…Tn}中的 T0 正在等待一個 T1 占用的資源,T1 正在等待 T2 占用的資源,以此類推,Tn 正在等待己被 T0 占用的資源。

如下圖所示
圖片描述

5. 死鎖的實現

為了更好的了解死鎖是如何產生的,我們首先來設計一個死鎖爭奪資源的場景。
場景設計

  • 創建 2 個線程,線程名分別為 threadA 和 threadB;
  • 創建兩個資源, 使用 new Object () 創建即可,分別命名為 resourceA 和 resourceB;
  • threadA 持有 resourceA 并申請資源 resourceB;
  • threadB 持有 resourceB 并申請資源 resourceA ;
  • 為了確保發生死鎖現象,請使用 sleep 方法創造該場景;
  • 執行代碼,看是否會發生死鎖。

期望結果:發生死鎖,線程 threadA 和 threadB 互相等待。

Tips:此處的實驗會使用到關鍵字 synchronized,后續小節還會對關鍵字 synchronized 單獨進行深入講解,此處對 synchronized 的使用僅僅為初級使用,有 JavaSE 基礎即可。

實例

public class DemoTest{
    private static  Object resourceA = new Object();//創建資源 resourceA
    private static  Object resourceB = new Object();//創建資源 resourceB

    public static void main(String[] args) throws InterruptedException {
        //創建線程 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 毫秒,確保此時 resourceB 已經進入run 方法的同步模塊
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "開始申請 resourceB。");
                    synchronized (resourceB) {
                        System.out.println (Thread.currentThread().getName() + "獲取 resourceB。");
                    }
                }
            }
        });
        threadA.setName("threadA");
        //創建線程 threadB
        Thread threadB = new Thread(new Runnable() { //創建線程 1
            @Override
            public void run() {
                synchronized (resourceB) {
                    System.out.println(Thread.currentThread().getName() + "獲取 resourceB。");
                    try {
                        Thread.sleep(1000); // sleep 1000 毫秒,確保此時 resourceA 已經進入run 方法的同步模塊
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "開始申請 resourceA。");
                    synchronized (resourceA) {
                        System.out.println (Thread.currentThread().getName() + "獲取 resourceA。");
                    }
                }
            }
        });
        threadB.setName("threadB");

        threadA. start();
        threadB. start();
    }
}

代碼講解

  • 從代碼中來看,我們首先創建了兩個資源 resourceA 和 resourceB;
  • 然后創建了兩條線程 threadA 和 threadB。threadA 首先獲取了 resourceA ,獲取的方式是代碼 synchronized (resourceA) ,然后沉睡 1000 毫秒;
  • 在 threadA 沉睡過程中, threadB 獲取了 resourceB,然后使自己沉睡 1000 毫秒;
  • 當兩個線程都蘇醒時,此時可以確定 threadA 獲取了 resourceA,threadB 獲取了 resourceB,這就達到了我們做的第一步,線程分別持有自己的資源;
  • 那么第二步就是開始申請資源,threadA 申請資源 resourceB,threadB 申請資源 resourceA 無奈 resourceA 和 resourceB 都被各自線程持有,兩個線程均無法申請成功,最終達成死鎖狀態。

執行結果驗證

threadA 獲取 resourceA。
threadB 獲取 resourceB。
threadA 開始申請 resourceB。
threadB 開始申請 resourceA。

看下驗證結果,發現已經出現死鎖,threadA 申請 resourceB,threadB 申請 resourceA,但均無法申請成功,死鎖得以實驗成功。

6. 如何避免線程死鎖

要想避免死鎖,只需要破壞掉至少一個構造死鎖的必要條件即可,學過操作系統的讀者應該都知道,目前只有請求并持有和環路等待條件是可以被破壞的。

造成死鎖的原因其實和申請資源的順序有很大關系,使用資源申請的有序性原則就可避免死鎖。

我們依然以第 5 個知識點進行講解,那么實驗的需求和場景不變,我們僅僅對之前的 threadB 的代碼做如下修改,以避免死鎖。

代碼修改

Thread threadB = new Thread(new Runnable() { //創建線程 1
            @Override
            public void run() {
                synchronized (resourceA) { //修改點 1
                    System.out.println(Thread.currentThread().getName() + "獲取 resourceB。");//修改點 3
                    try {
                        Thread.sleep(1000); // sleep 1000 毫秒,確保此時 resourceA 已經進入run 方法的同步模塊
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "開始申請 resourceA。");//修改點 4
                    synchronized (resourceB) { //修改點 2
                        System.out.println (Thread.currentThread().getName() + "獲取 resourceA。"); //修改點 5
                    }
                }
            }
        });

請看如上代碼示例,有 5 個修改點:

  • 修改點 1 :將 resourceB 修改成 resourceA;
  • 修改點 2 :將 resourceA 修改成 resourceB;
  • 修改點 3 :將 resourceB 修改成 resourceA;
  • 修改點 4 :將 resourceA 修改成 resourceB;
  • 修改點 5 :將 resourceA 修改成 resourceB。

請讀者按指示修改代碼,并從新運行驗證。

修改后代碼講解

  • 從代碼中來看,我們首先創建了兩個資源 resourceA 和 resourceB;
  • 然后創建了兩條線程 threadA 和 threadB。threadA 首先獲取了 resourceA ,獲取的方式是代碼 synchronized (resourceA) ,然后沉睡 1000 毫秒;
  • 在 threadA 沉睡過程中, threadB 想要獲取 resourceA ,但是 resourceA 目前正被沉睡的 threadA 持有,所以 threadB 等待 threadA 釋放 resourceA;
  • 1000 毫秒后,threadA 蘇醒了,釋放了 resourceA ,此時等待的 threadB 獲取到了 resourceA,然后 threadB 使自己沉睡 1000 毫秒;
  • threadB 沉睡過程中,threadA 申請 resourceB 成功,繼續執行成功后,釋放 resourceB;
  • 1000 毫秒后,threadB 蘇醒了,繼續執行獲取 resourceB ,執行成功。

執行結果驗證

threadA 獲取 resourceA。
threadA 開始申請 resourceB。
threadA 獲取 resourceB。
threadB 獲取 resourceA。
threadB 開始申請 resourceB。
threadB 獲取 resourceB。

我們發現 threadA 和 threadB 按照相同的順序對 resourceA 和 resourceB 依次進行訪問,避免了互相交叉持有等待的狀態,避免了死鎖的發生。

7. 小結

死鎖是并發編程中最致命的問題,如何避免死鎖,是并發編程中恒久不變的問題。
掌握死鎖的實現以及如果避免死鎖的發生,是本節內容的重中之重。