詳細分析 Java 中啓動線程的正確和錯誤方式

啓動線程的正確和錯誤方式

前文回顧

  1. 詳細分析 Java 中實現多線程的方法有幾種?(從本質上出發)

start 方法和 run 方法的比較

代碼演示:html

/**
 * <p>
 * start() 和 run() 的比較
 * </p>
 *
 * @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();
    }
}

從以上示例能夠分析出如下兩點:java

  • 直接使用 run 方法不會啓動一個新線程。(錯誤方式)多線程

  • start 方法會啓動一個新線程。(正確方式)ide

start 方法分析

start 方法的含義以及注意事項

  • start 方法能夠啓動一個新線程。源碼分析

    • 線程對象在初始化以後調用了 start 方法以後, 當前線程(一般是主線程)會請求 JVM 虛擬機若是有空閒的話來啓動一下這邊的這個新線程。this

    • 也就是說, 啓動一個新線程的本質就是請求 JVM 來運行這個線程。操作系統

    • 至於這個線程什麼時候可以運行,並非簡單的由咱們可以決定的,而是由線程調度器去決定的。線程

    • 若是它很忙,即便咱們運行了 start 方法,也不必定可以馬上的啓動線程。code

    • 因此說 srtart 方法調用以後,並不意味這個方法已經開始運行了。它可能稍後纔會運行,也頗有可能很長時間都不會運行,好比說遇到了飢餓的狀況。htm

    • 這也就印證了有些狀況下,線程 1 先掉用了 start 方法,而線程 2 後調用了 start 方法,卻發現線程 2 先執行線程 1 後執行的狀況。

    • 總結: 調用 start 方法的順序並不能決定真正線程執行的順序。

    • 注意事項

      • start 方法會牽扯到兩個線程。

      • 第一個就是主線程,由於咱們必需要有一個主線程或者是其餘的線程(哪怕不是主線程)來執行這個 start 方法,第二個纔是新的線程。

      • 不少狀況下會忽略掉爲咱們建立線程的這個主線程,不要誤覺得調用了 start 就已是子線程去執行了,這個語句實際上是主線程或者說是父線程來執行的,被執行以後纔去建立新線程。

  • start 方法建立新線程的準備工做

    • 首先,它會讓本身處於就緒狀態。

      • 就緒狀態指已經獲取到除了 CPU 之外的其餘資源, 如已經設置了上下文、棧、線程狀態以及 PC(PC 是一個寄存器,PC 指向程序運行的位置) 等。
    • 作完這些準備工做以後,就萬事俱備只欠東風了,東風就是 CPU 資源。

    • 作完準備工做以後,線程才能被 JVM 或操做系統進一步去調度到執行狀態等待獲取 CPU 資源,而後纔會真正地進入到運行狀態執行 run 方法中的代碼。

  • 須要注意: 不能重複的執行 start 方法

    • 代碼示例

      /**
      * <p>
      * 演示不能重複的執行 start 方法(兩次及以上), 不然會報錯
      * </p>
      *
      * @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 方法源碼分析

源碼

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 */
        }
    }
}

源碼中的流程

第一步:
啓動新線程時會首先檢查線程狀態是否爲初始狀態, 這也是以上拋出異常的緣由。即如下代碼:

if (threadStatus != 0)
	throw new IllegalThreadStateException();

其中 threadStatus 這個變量的註釋以下,也就是說 Java 的線程狀態最初始(尚未啓動)的時候表示爲 0:

/* Java thread status for tools,
 * initialized to indicate thread 'not yet started'
 */
private volatile int threadStatus = 0;

第二步:
將其加入線程組。即如下代碼:

group.add(this);

第三步:
最後調用 start0() 這個 native 方法(native 表明它的代碼不是由 Java 實現的,而是由 C/C++ 實現的,具體實現能夠在 JDK 裏面看到,瞭解便可), 即如下代碼:

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 方法源碼分析

@Override
public void run() {
    // 傳入了 target 對象(即 Runnable 接口的實現), 執行傳入的 target 對象的 run 方法
    if (target != null) {
        target.run();
    }
}

對於 run 方法的兩種狀況

  • 第一種: 重寫了 Thread 類的 run 方法,Threadrun 方法會失效, 將會執行重寫的 run 方法。

  • 第二種: 傳入了 target 對象(即 Runnable 接口的實現),執行 Thread 的原有 run 方法而後接着執行 target 對象的 run 方法。

  • 總結:

    • run 方法就是一個普通的方法, 上文中直接去執行 run 方法也就是至關於咱們執行本身寫的普通方法同樣,因此它的執行線程就是咱們的主線程。

    • 因此要想真正的啓動線程,不能直接調用 run 方法,而是要調用 start 方法,其中能夠間接的調用 run 方法。


若有寫的不足的,請見諒,請你們多多指教。