1. 程式人生 > >詳細分析 Java 中啟動執行緒的正確和錯誤方式

詳細分析 Java 中啟動執行緒的正確和錯誤方式

[TOC] # 啟動執行緒的正確和錯誤方式 ## 前文回顧 1. [詳細分析 Java 中實現多執行緒的方法有幾種?(從本質上出發)](https://www.cnblogs.com/txxunmei/p/13733332.html) ## start 方法和 run 方法的比較 **程式碼演示:** ```java /** *

* start() 和 run() 的比較 *

* * @author 踏雪彡尋梅 * @version 1.0 * @date 2020/9/20 - 16:15 * @since JDK1.8 */ public class StartAndRunMethod { public static void main(String[] args) { // run 方法演示 // 輸出: name: main // 說明由主執行緒去執行的, 不符合新建一個執行緒的本意 Runnable runnable = () -> { System.out.println("name: " + Thread.currentThread().getName()); }; runnable.run(); // start 方法演示 // 輸出: name: Thread-0 // 說明新建了一個執行緒, 符合本意 new Thread(runnable).start(); } } ``` 從以上示例可以分析出以下兩點: - 直接使用 `run` 方法不會啟動一個新執行緒。(錯誤方式) - `start` 方法會啟動一個新執行緒。(正確方式) ## start 方法分析 ### start 方法的含義以及注意事項 - `start` 方法可以啟動一個新執行緒。 - 執行緒物件在初始化之後呼叫了 `start` 方法之後, 當前執行緒(通常是主執行緒)會請求 JVM 虛擬機器如果有空閒的話來啟動一下這邊的這個新執行緒。 - 也就是說, 啟動一個新執行緒的本質就是請求 JVM 來執行這個執行緒。 - 至於這個執行緒何時能夠執行,並不是簡單的由我們能夠決定的,而是由執行緒排程器去決定的。 - 如果它很忙,即使我們運行了 `start` 方法,也不一定能夠立刻的啟動執行緒。 - 所以說 `srtart` 方法呼叫之後,並不意味這個方法已經開始運行了。它可能稍後才會執行,也很有可能很長時間都不會執行,比如說遇到了飢餓的情況。 - 這也就印證了有些情況下,執行緒 1 先掉用了 `start` 方法,而執行緒 2 後呼叫了 `start` 方法,卻發現執行緒 2 先執行執行緒 1 後執行的情況。 - 總結: 呼叫 `start` 方法的順序並不能決定真正執行緒執行的順序。 - **注意事項** - `start` 方法會牽扯到兩個執行緒。 - 第一個就是主執行緒,因為我們必須要有一個主執行緒或者是其他的執行緒(哪怕不是主執行緒)來執行這個 `start` 方法,第二個才是新的執行緒。 - 很多情況下會忽略掉為我們建立執行緒的這個主執行緒,不要誤以為呼叫了 `start` 就已經是子執行緒去執行了,這個語句其實是主執行緒或者說是父執行緒來執行的,被執行之後才去建立新執行緒。 - `start` 方法建立新執行緒的準備工作 - 首先,它會讓自己處於就緒狀態。 - 就緒狀態指已經獲取到除了 CPU 以外的其他資源, 如已經設定了上下文、棧、執行緒狀態以及 PC(PC 是一個暫存器,PC 指向程式執行的位置) 等。 - 做完這些準備工作之後,就萬事俱備只欠東風了,東風就是 CPU 資源。 - 做完準備工作之後,執行緒才能被 JVM 或作業系統進一步去排程到執行狀態等待獲取 CPU 資源,然後才會真正地進入到執行狀態執行 `run` 方法中的程式碼。 - **需要注意: 不能重複的執行 start 方法** - 程式碼示例 ```java /** *

* 演示不能重複的執行 start 方法(兩次及以上), 否則會報錯 *

* * @author 踏雪彡尋梅 * @version 1.0 * @date 2020/9/20 - 16:47 * @since JDK1.8 */ public class CantStartTwice { public static void main(String[] args) { Runnable runnable = () -> { System.out.println("name: " + Thread.currentThread().getName()); }; Thread thread = new Thread(runnable); // 輸出: name: Thread-0 thread.start(); // 輸出: 丟擲 java.lang.IllegalThreadStateException // 即非法執行緒狀態異常(執行緒狀態不符合規定) thread.start(); } } ``` - 報錯的原因 - `start` 一旦開始執行,執行緒狀態就從最開始的 New 狀態進入到後續的狀態,比如說 Runnable,然後一旦執行緒執行完畢,執行緒就會變成終止狀態,而終止狀態永遠不可能再返回回去,所以會丟擲以上異常,也就是說不能回到初始狀態了。這裡描述的還不夠清晰,讓我們來看看原始碼能瞭解的更透徹。 ### start 方法原始碼分析 #### 原始碼 ```java public synchronized void start() { /** * This method is not invoked for the main method thread or "system" * group threads created/set up by the VM. Any new functionality added * to this method in the future may have to also be added to the VM. * * A zero status value corresponds to state "NEW". */ // 第一步, 檢查執行緒狀態是否為初始狀態, 這裡也就是上面丟擲異常的原因 if (threadStatus != 0) throw new IllegalThreadStateException(); /* Notify the group that this thread is about to be started * so that it can be added to the group's list of threads * and the group's unstarted count can be decremented. */ // 第二步, 加入執行緒組 group.add(this); boolean started = false; try { // 第三步, 呼叫 start0 方法 start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { /* do nothing. If start0 threw a Throwable then it will be passed up the call stack */ } } } ``` #### 原始碼中的流程 **第一步:** 啟動新執行緒時會首先檢查執行緒狀態是否為初始狀態, 這也是以上丟擲異常的原因。即以下程式碼: ```java if (threadStatus != 0) throw new IllegalThreadStateException(); ``` 其中 `threadStatus` 這個變數的註釋如下,也就是說 Java 的執行緒狀態最初始(還沒有啟動)的時候表示為 0: ```java /* Java thread status for tools, * initialized to indicate thread 'not yet started' */ private volatile int threadStatus = 0; ``` **第二步:** 將其加入執行緒組。即以下程式碼: ```java group.add(this); ``` **第三步:** 最後呼叫 `start0()` 這個 native 方法(native 代表它的程式碼不是由 Java 實現的,而是由 C/C++ 實現的,具體實現可以在 JDK 裡面看到,瞭解即可), 即以下程式碼: ```java boolean started = false; try { // 第三步, 呼叫 start0 方法 start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { /* do nothing. If start0 threw a Throwable then it will be passed up the call stack */ } } ``` ## run 方法分析 ### run 方法原始碼分析 ```java @Override public void run() { // 傳入了 target 物件(即 Runnable 介面的實現), 執行傳入的 target 物件的 run 方法 if (target != null) { target.run(); } } ``` ### 對於 run 方法的兩種情況 - 第一種: 重寫了 `Thread` 類的 `run` 方法,`Thread` 的 `run` 方法會失效, 將會執行重寫的 `run` 方法。 - 第二種: 傳入了 `target` 物件(即 `Runnable` 介面的實現),執行 `Thread` 的原有 `run` 方法然後接著執行 `target` 物件的 `run` 方法。 - 總結: - `run` 方法就是一個普通的方法, 上文中直接去執行 `run` 方法也就是相當於我們執行自己寫的普通方法一樣,所以它的執行執行緒就是我們的主執行緒。 - 所以要想真正的啟動執行緒,不能直接呼叫 `run` 方法,而是要呼叫 `start` 方法,其中可以間接的呼叫 `run` 方法。 --- **如有寫的不足的,請見諒,請大家多多指