Java 原始碼刨析 - 執行緒的狀態有哪些?它是如何工作的?

流年的夏天發表於2020-06-18

執行緒(Thread)是併發程式設計的基礎,也是程式執行的最小單元,它依託程式而存在

一個程式中可以包含多個執行緒,多執行緒可以共享一塊記憶體空間和一組系統資源,因此執行緒之間的切換更加節省資源、更加輕量化,也因此被稱為輕量級的程式。

   

執行緒的狀態在 JDK 1.5 之後以列舉的方式被定義在 Thread 的原始碼中,它總共包含以下 6 個狀態:

NEW新建狀態,執行緒被建立出來,但尚未啟動時的執行緒狀態;

RUNNABLE就緒狀態,表示可以執行的執行緒狀態,它可能正在執行,或者是在排隊等待作業系統給它分配 CPU 資源;

BLOCKED阻塞等待鎖的執行緒狀態,表示處於阻塞狀態的執行緒正在等待監視器鎖,比如等待執行 synchronized 程式碼塊或者使用 synchronized 標記的方法;

WAITING等待狀態,一個處於等待狀態的執行緒正在等待另一個執行緒執行某個特定的動作,比如,一個執行緒呼叫了 Object.wait() 方法,那它就在等待另一個執行緒呼叫 Object.notify() Object.notifyAll() 方法;

TIMED_WAITING計時等待狀態,和等待狀態(WAITING)類似,它只是多了超時時間,比如呼叫了有超時時間設定的方法 Object.wait(long timeout) Thread.join(long timeout) 等這些方法時,它才會進入此狀態;

TERMINATED終止狀態,表示執行緒已經執行完成。

執行緒狀態的原始碼如下:

public enum State {

    /**

     * 新建狀態,執行緒被建立出來,但尚未啟動時的執行緒狀態

     */

    NEW,

   

    /**

     * 就緒狀態,表示可以執行的執行緒狀態,但它在排隊等待來自作業系統的 CPU 資源

     */

    RUNNABLE,

   

    /**

     * 阻塞等待鎖的執行緒狀態,表示正在處於阻塞狀態的執行緒

     * 正在等待監視器鎖,比如等待執行 synchronized 程式碼塊或者

     * 使用 synchronized 標記的方法

     */

    BLOCKED,

   

    /**

     * 等待狀態,一個處於等待狀態的執行緒正在等待另一個執行緒執行某個特定的動作。

     * 例如,一個執行緒呼叫了 Object.wait() 它在等待另一個執行緒呼叫

     * Object.notify() 或 Object.notifyAll()

     */

    WAITING,

   

    /**

     * 計時等待狀態,和等待狀態 (WAITING) 類似,只是多了超時時間,比如

     * 呼叫了有超時時間設定的方法 Object.wait(long timeout) 和 

     * Thread.join(long timeout) 就會進入此狀態

     */

    TIMED_WAITING,

   

    /**

     * 終止狀態,表示執行緒已經執行完成

     */

}

   

執行緒的工作模式是,首先先要建立執行緒並指定執行緒需要執行的業務方法,然後再呼叫執行緒的 start() 方法,此時執行緒就從 NEW(新建)狀態變成了 RUNNABLE(就緒)狀態;

然後執行緒會判斷要執行的方法中有沒有 synchronized 同步程式碼塊,如果有並且其他執行緒也在使用此鎖,那麼執行緒就會變為 BLOCKED(阻塞等待)狀態,當其他執行緒使用完此鎖之後,執行緒會繼續執行剩餘的方法。

   

當遇到 Object.wait() Thread.join() 方法時,執行緒會變為 WAITING(等待狀態)狀態;

如果是帶了超時時間的等待方法,那麼執行緒會進入 TIMED_WAITING(計時等待)狀態;

當有其他執行緒執行了 notify() notifyAll() 方法之後,執行緒被喚醒繼續執行剩餘的業務方法,直到方法執行完成為止,此時整個執行緒的流程就執行完了,執行流程如下圖所示:

BLOCKED WAITING 的區別】

雖然 BLOCKED WAITING 都有等待的含義,但二者有著本質的區別。

首先它們狀態形成的呼叫方法不同

其次 BLOCKED 可以理解為當前執行緒還處於活躍狀態,只是在阻塞等待其他執行緒使用完某個鎖資源

WAITING 則是因為自身呼叫 Object.wait() 或著是 Thread.join() 又或者是 LockSupport.park() 而進入等待狀態,只能等待其他執行緒執行某個特定的動作才能被繼續喚醒。

比如當執行緒因為呼叫了 Object.wait() 而進入 WAITING 狀態之後,則需要等待另一個執行緒執行 Object.notify() Object.notifyAll() 才能被喚醒。

   

start() run() 的區別】

首先從 Thread 原始碼來看,start() 方法屬於 Thread 自身的方法,並且使用了 synchronized 來保證執行緒安全,原始碼如下:

public synchronized void start() {

    // 狀態驗證,不等於 NEW 的狀態會丟擲異常

    if (threadStatus != 0)

        throw new IllegalThreadStateException();

    // 通知執行緒組,此執行緒即將啟動

   

    group.add(this);

    boolean started = false;

    try {

        start0();

        started = true;

    } finally {

        try {

            if (!started) {

                group.threadStartFailed(this);

            }

        } catch (Throwable ignore) {

            // 不處理任何異常,如果 start0 丟擲異常,則它將被傳遞到呼叫堆疊上

        }

    }

}

   

run() 方法為 Runnable 的抽象方法,必須由呼叫類重寫此方法,重寫的 run() 方法其實就是此執行緒要執行的業務方法,原始碼如下:

public class Thread implements Runnable {

 // 忽略其他方法......

  private Runnable target;

  @Override

  public void run() {

      if (target != null) {

          target.run();

      }

  }

}

@FunctionalInterface

public interface Runnable {

    public abstract void run();

}

   

從執行的效果來說,start() 方法可以開啟多執行緒,讓執行緒從 NEW 狀態轉換成 RUNNABLE 狀態,而 run() 方法只是一個普通的方法。

   

其次,它們可呼叫的次數不同,start() 方法不能被多次呼叫,否則會丟擲 java.lang.IllegalStateException;而 run() 方法可以進行多次呼叫,因為它只是一個普通的方法而已。

   

【執行緒優先順序】

Thread 原始碼中和執行緒優先順序相關的屬性有 3 個:

// 執行緒可以擁有的最小優先順序

public final static int MIN_PRIORITY = 1;

   

// 執行緒預設優先順序

public final static int NORM_PRIORITY = 5;

   

// 執行緒可以擁有的最大優先順序

public final static int MAX_PRIORITY = 10

   

執行緒的優先順序可以理解為執行緒搶佔 CPU 時間片的概率,優先順序越高的執行緒優先執行的概率就越大,但並不能保證優先順序高的執行緒一定先執行。

   

在程式中我們可以通過 Thread.setPriority() 來設定優先順序,setPriority() 原始碼如下:

public final void setPriority(int newPriority) {

    ThreadGroup g;

    checkAccess();

    // 先驗證優先順序的合理性

    if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {

        throw new IllegalArgumentException();

    }

    if((g = getThreadGroup()) != null) {

        // 優先順序如果超過執行緒組的最高優先順序,則把優先順序設定為執行緒組的最高優先順序

        if (newPriority > g.getMaxPriority()) {

            newPriority = g.getMaxPriority();

        }

        setPriority0(priority = newPriority);

    }

}

   

【執行緒的常用方法】

執行緒的常用方法有以下幾個。

   

join()

一個執行緒中呼叫 other.join() ,這時候當前執行緒會讓出執行權給 other 執行緒,直到 other 執行緒執行完或者過了超時時間之後再繼續執行當前執行緒,join() 原始碼如下:

public final synchronized void join(long millis)

throws InterruptedException {

    long base = System.currentTimeMillis();

    long now = 0;

    // 超時時間不能小於 0

    if (millis < 0) {

        throw new IllegalArgumentException("timeout value is negative");

    }

    // 等於 0 表示無限等待,直到執行緒執行完為之

    if (millis == 0) {

        // 判斷子執行緒 (其他執行緒) 為活躍執行緒,則一直等待

        while (isAlive()) {

            wait(0);

        }

    } else {

        // 迴圈判斷

        while (isAlive()) {

            long delay = millis - now;

            if (delay <= 0) {

                break;

            }

            wait(delay);

            now = System.currentTimeMillis() - base;

        }

    }

}

   

從原始碼中可以看出 join() 方法底層還是通過 wait() 方法來實現的。

   

例如,在未使用 join() 時,程式碼如下:

public class ThreadExample {

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(() -> {

            for (int i = 1; i < 6; i++) {

                try {

                    Thread.sleep(1000);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

                System.out.println("子執行緒睡眠:" + i + "秒。");

            }

        });

        thread.start(); // 開啟執行緒

        // 主執行緒執行

        for (int i = 1; i < 4; i++) {

            try {

                Thread.sleep(1000);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

            System.out.println("主執行緒睡眠:" + i + "秒。");

        }

    }

}

程式執行結果為:

複製主執行緒睡眠:1秒。

子執行緒睡眠:1秒。

主執行緒睡眠:2秒。

子執行緒睡眠:2秒。

主執行緒睡眠:3秒。

子執行緒睡眠:3秒。

子執行緒睡眠:4秒。

子執行緒睡眠:5秒。

   

從結果可以看出,在未使用 join() 時主子執行緒會交替執行。

   

然後我們再把 join() 方法加入到程式碼中,程式碼如下:

public class ThreadExample {

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(() -> {

            for (int i = 1; i < 6; i++) {

                try {

                    Thread.sleep(1000);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

                System.out.println("子執行緒睡眠:" + i + "秒。");

            }

        });

        thread.start(); // 開啟執行緒

        thread.join(2000); // 等待子執行緒先執行 2 秒鐘

        // 主執行緒執行

        for (int i = 1; i < 4; i++) {

            try {

                Thread.sleep(1000);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

            System.out.println("主執行緒睡眠:" + i + "秒。");

        }

    }

}

程式執行結果為:

   

複製子執行緒睡眠:1秒。

子執行緒睡眠:2秒。

主執行緒睡眠:1秒。 

// thread.join(2000); 等待 2 秒之後,主執行緒和子執行緒再交替執行

子執行緒睡眠:3秒。

主執行緒睡眠:2秒。

子執行緒睡眠:4秒。

子執行緒睡眠:5秒。

主執行緒睡眠:3秒。

從執行結果可以看出,新增 join() 方法之後,主執行緒會先等子執行緒執行 2 秒之後才繼續執行。

   

yield()

Thread 的原始碼可以知道 yield() 為本地方法,也就是說 yield() 是由 C C++ 實現的,原始碼如下:

public static native void yield();

   

yield() 方法表示給執行緒排程器一個當前執行緒願意出讓 CPU 使用權的暗示,但是執行緒排程器可能會忽略這個暗示。

   

比如我們執行這段包含了 yield() 方法的程式碼,如下所示:

public static void main(String[] args) throws InterruptedException {

    Runnable runnable = new Runnable() {

        @Override

        public void run() {

            for (int i = 0; i < 10; i++) {

                System.out.println("執行緒:" +

                        Thread.currentThread().getName() + " I" + i);

                if (i == 5) {

                    Thread.yield();

                }

            }

        }

    };

    Thread t1 = new Thread(runnable, "T1");

    Thread t2 = new Thread(runnable, "T2");

    t1.start();

    t2.start();

}

   

當我們把這段程式碼執行多次之後會發現,每次執行的結果都不相同,這是因為 yield() 執行非常不穩定,執行緒排程器不一定會採納 yield() 出讓 CPU 使用權的建議,從而導致了這樣的結果。

相關文章