並發王者課-青銅8:分工協作-從本質認知線程的狀態和動作方法


歡迎來到《並發王者課》,本文是該系列文章中的第8篇

在本篇文章中,我將從多線程的本質出發,為你介紹線程相關的狀態和它們的變遷方式,並幫助你掌握這塊知識點

一、多線程的本質是分工協作

如果你是王者的玩家,那么你一定知道王者中的眾多英雄分為主要分為幾類,比如法師戰士坦克輔助等等。一些玩家對這些分類可能並不了解,甚至會覺得,干嘛要搞得這么復雜,干不完了嘛。這...當然不可以

抱此想法的如果不是青銅玩家,想必就是戰場上的那些個人英雄主義玩家,在他們眼里沒有團隊。然而,只有王者知道,比賽勝利的關鍵,在於團隊的分工協作各自為戰必將一團亂麻、潰不成軍,正所謂單絲不成線,獨木難成林

分工協作無處不在,峽谷中需要分工協作,現實中我們的工作更是社會化分工的結果,因為社會的本質就是分工協作

而我要告訴你的是,在並發編程里,多線程的本質也是分工協作,每個線程恰似一個英雄,有着自己的職責、狀態和技能(動作方法)。所謂線程的狀態、方法實現不過都是為了完成線程間的分工協作。換句話說,線程狀態的存在不是目的,而是實現分工協作的方式。所以,理解線程的線程狀態和驅動方法,首先要理解它們為什么而存在

IMG_5120

二、從協作認知線程的狀態

線程的狀態是線程在協作過程中的瞬時特征。根據協作的需要,線程總共有六種狀態,分別是NEWRUNNABLEWAITINGTIMED_WAITINGBLOCKEDTERMINATED等。比如,我們創建一個英雄哪吒的線程neZhaPlayer

Thread neZhaPlayer = new Thread(new NeZhaRunnable());

那么,線程創建之后,接下來它將在下圖所示的六種狀態中變遷。剛創建的線程處於NEW的狀態,而如果我們調用neZhaPlayer.start(),那它將會進入RUNNABLE狀態。

六種不同狀態的含義是這樣的:

  • NEW:線程新建但尚未啟動時所處的狀態,比如上面的neZhaPlayer
  • RUNNABLE:在 Java 虛擬機中執行的線程所處狀態。需要注意的是,雖然線程當前正在被執行,但可能正在等待其他線程釋放資源;
  • WAITING無限期等待另一個線程執行特定操作來解除自己的等待狀態;
  • TIMED_WAITING限時等待另一個線程執行或自我解除等待狀態;
  • BLOCKED被阻塞等待其他線程釋放Monitor Lock;
  • TERMINATED:線程執行結束。

在任意特定時刻,一個線程都只能處於上述六種狀態中的一種。需要你注意的是RUNNABLE這個狀態,它有些特殊。確切地說,它包含READYRUNNING兩個細分狀態,下一章節的圖示中有明確標示。

另外,前面我們已經介紹過Thread類,對於線程各狀態的表述,你可以直接閱讀JDK中的Thread.State枚舉,並可以通過Thread.getState()查看當前線程的瞬時狀態。

三、從線程狀態變遷看背后的方法驅動

和人類的交流類似,在多線程的協作時,它們也需要交流。所以,線程狀態的變遷需就要不同的方法來實現交流,比如剛創建的線程需要通過調用start()將線程狀態由NEW變遷為RUNNABLE

下圖所展示的正是線程間的狀態變遷以及相關的驅動方法,你可以先大概瀏覽一遍,隨后再結合下文的各關鍵方法的表述深入理解。

需要注意的是,本文不會詳細介紹線程狀態相關的所有方法,這既不現實也毫無必要。上面這幅寶藏圖示是理解本文所述知識的核心,下面所介紹的幾個主要方法也並非為了你記憶,而是為了讓你更好理解上面這幅圖

在你理解了這幅寶圖之后,你便可以完全自行去了解其他更多的方法。

1. start:對戰開始,敵軍還有5秒到達戰場

public class NeZhaRunnable implements Runnable {
    public void run() {
        System.out.println("我是哪吒,我去上路");
    }
}

Thread neZhaPlayer = new Thread(new NeZhaRunnable());
neZhaPlayer.start();

start()方法主要將完成線程狀態從NEWRUNNABLE的變遷,這里有兩個點:

  • 創建新的線程;
  • 由新的線程執行其中的run()方法。

需要注意的是,你不可以重復調用start()方法,否則會拋出IllegalThreadStateException異常。

2. wait和notify:我在等你,好了請告訴我

哪吒每次在使用完大招后,都需要經歷幾十秒的冷卻時間才可以再次使用,接下來我們通過代碼片段來模擬這個過程。

我們先定義一個Player類,這個類中包含了fight()refreshSkills()兩個方法,分別用於進攻和技能刷新,代碼片段如下。

public class Player {
    public void fight() {
        System.out.println("大招未就緒,冷卻中...");
        synchronized (this) {
            try {
                this.wait();
                System.out.println("大招已就緒,發起進攻!");
            } catch (InterruptedException e) {
                System.out.println("大招冷卻被中斷!");
            }
        }
    }
    public void refreshSkills() {
        System.out.println("技能刷新中...");
        synchronized (this) {
            this.notifyAll();
            System.out.println("技能已刷新!");
        }
    }
}

隨后,我們寫一段main()方法使用剛才創建的Player。注意,這里我們創建了兩個線程分別調用Player中的不同方法

public static void main(String[] args) throws InterruptedException {
        final Player neZha = new Player();
        Thread neZhaFightThread = new Thread() {
            public void run() {
                neZha.fight();
            }
        };
        Thread skillRefreshThread = new Thread() {
            public void run() {
                neZha.refreshSkills();
            }
        };
        neZhaFightThread.start();
        skillRefreshThread.start();
    }

代碼運行結果如下:

大招未就緒,冷卻中...
技能刷新中...
技能已刷新!
大招已就緒,發起進攻!

Process finished with exit code 0

從運行的結果看,符合預期。相信你已經看到了,在上面的代碼中我們使用了wait()notify()兩個函數。這兩個線程是如何協作的呢?往下看。

首先,neZhaAttachThread調用了neZha.fight()這個方法。可是,當哪吒想發起進攻的時候,竟然大招還沒有冷卻!於是,這個線程不得不通過wait()方法進入等待隊列

緊接着,skillRefreshThread調用了neZha.refreshSkills()這個方法。並且,在執行結束后又調用了notify()方法。有趣的事情發生了,前面處於等待隊列中的neZhaAttachThread竟然又“復活”了,並且大喊了一聲:大招已經就緒,發起進攻!

這是怎么回事?理解這塊邏輯,你需要了解以下幾個知識點:

  • wait():看到wait()時,你可以簡單粗暴地認為每個對象都有一個類似於休息室的等待隊列,而wait()正是把當前線程送進了等待隊列並暫停繼續執行;
  • notify():如果說wait()是把當前線程送進了等待隊列,那么notify()則是從等待隊列中取出線程。此外,和notify()具有相似功能的還有個notifyAll()。與notify()不同的是,notifyAll()會取出等待隊列中的所有線程;

看到這,你是不是覺得wait()notify()簡直是完美的一對?其實不然。真相不僅不完美,還很不靠譜!

wait()notify()在執行時都必須先獲得鎖,這也是你在代碼中看到synchronized的原因。notify()在釋放鎖的時候,會從等待隊列中取出線程,此時的線程必須獲得鎖之后才能繼續運行。那么,問題來了。如果隊列中有多個線程時,notify()能取出指定的線程嗎?答案是不能!

換句話說,如果隊列中有多個線程,你將無法預料后續的執行結果!notifyAll()雖然可以取出所有的線程,但最終也只能有一個線程能獲得鎖。

是不是有點懵?懵就對了。所以你看,wait()notify()是不是很不靠譜?因此,如果你需要在項目代碼中使用它們,請務必要小心謹慎!

此外,如果你閱讀過《Effective Java》,可以看到在這本書里作者Josh Bloch也是強烈建議不要隨便使用這對組合。因為它們就像Java中的“匯編語言”,確實復雜且不容易控制,如果有相似的並發場景需要處理,可以考慮使用Java中的其他高級的並發工具。

3. interrupt:做完這一單,我就退隱江湖

在王者的游戲中,如果英雄血量沒了,可以回城補血。回城大概需要5秒左右,如果在回城的過程中,突然被攻擊或需要移位,那么回城就會中斷。接下來,下面我們看看怎么模擬回城中的中斷。

現在Player中定義backHome()方法用於回城。

 public void backHome() {
        System.out.println("回城中...");
        synchronized (this) {
            try {
                this.wait();
                System.out.println("已回城");
            } catch (InterruptedException e) {
                System.out.println("回城被中斷!");
            }
        }
    }

接下來啟動新的線程調用backHome()回城補血。

public static void main(String[] args) throws InterruptedException {
        final Player neZha = new Player();
        Thread neZhaBackHomeThread = new Thread() {
            public void run() {
                neZha.backHome();
            }
        };
        neZhaBackHomeThread.start();
        neZhaBackHomeThread.interrupt();
    }

運行結果如下:

回城中...
回城被中斷!

Process finished with exit code 0

可以看到,回城被中斷了,因為我們調用了interrupt()方法!那么,在線程中的中斷是怎么回事?往下看。

在Thread中,我們可以通過interrupt()中斷線程。然而,如果你細心的話,還會發現Thread中除了interrupt()方法之外,竟然還有兩個長相酷似的方法:interrupted()isInterrupted()。這就要小心了。

  • interrupt():將線程設置為中斷狀態;
  • interrupted():取消線程的中斷狀態;
  • isInterrupted():判斷線程是否處於中斷狀態,而不會變更線程狀態。

不得不說,interrupt()interrupted()這兩個方法的命名實在糟糕,你在編碼時可不要學習它,方法的名字應該清晰明了表達出其意圖

那么,當我們調用interrupt()時,所調用對象的線程會立即拋出InterruptedException異常嗎?其實不然,這里容易產生誤解

interrupt()方法只是改變了線程中的中斷狀態而已,並不會直接拋出中斷異常。中斷異常的拋出必須是當前線程在執行wait()sleep()join()時才會拋出。換句話說,如果當前線程正在處理其他的邏輯運算,不會被中斷,直到下次運行wait()sleep()join()

4. join:稍等,等我結束你再開始

在前面的示例中,哪吒發起進攻和技能刷新兩個線程是同時開始的。然而,我們在前面已經說了wait()notify()並不靠譜,所以我們想在技能刷新結束后再執行后續動作。

public static void main(String[] args) throws InterruptedException {
        final Player neZha = new Player();
        Thread neZhaFightThread = new Thread() {
            public void run() {
                neZha.fight();
            }
        };
        Thread skillRefreshThread = new Thread() {
            public void run() {
                neZha.refreshSkills();
            }
        };
       
        skillRefreshThread.start();
        skillRefreshThread.join(); //這里是重點
        neZhaFightThread.start();
    }

主線程調用join()時,會阻塞當前線程繼續運行,直到目標線程中的任務執行完畢。此外,在調用join()方法時,也可以設置超時時間。

小結

以上就是關於線程狀態及變遷的全部內容。在本文中,我們介紹了多線程的本質是協作,而狀態和動作方法是實現協作的方式。無論是面試還是其他的資料中,線程的狀態方法都是重點。然而,我希望你明白了的是,對於本文知識點的掌握,不要從靜態的角度死記硬背,而是要動靜結合,從動態的方法認知靜態的狀態

正文到此結束,恭喜你又上了一顆星✨

夫子的試煉

在本文中,我們並沒有提到yield()Thread.sleep()Thread.current()等方法。不過,如果你感興趣的話,不妨檢索資料:

  • 了解yield()並對比它和join()的不同;
  • 了解wait()並對比它和Thread.sleep()的不同;
  • 了解Thread.current()的主要用法和它的實現。

延伸閱讀


關於作者


關注公眾號【庸人技術笑談】,獲取及時文章更新。記錄平凡人的技術故事,分享有品質(盡量)的技術文章,偶爾也聊聊生活和理想。不販賣焦慮,不做標題黨。

如果本文對你有幫助,歡迎點贊關注監督,我們一起從青銅到王者


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM