1. 程式人生 > 實用技巧 >第四章、Java執行緒的狀態及主要轉化方法

第四章、Java執行緒的狀態及主要轉化方法

一、作業系統中的執行緒狀態轉換

  首先我們來看看作業系統中的執行緒狀態轉換。

在現在的作業系統中,執行緒是被視為輕量級程序的,所以作業系統執行緒的狀態其實和作業系統程序的狀態是一致的

作業系統執行緒主要有以下三個狀態:

  • 就緒狀態(ready):執行緒正在等待使用CPU,經排程程式呼叫之後可進入running狀態。

  • 執行狀態(running):執行緒正在使用CPU。

  • 等待狀態(waiting): 執行緒經過等待事件的呼叫或者正在等待其他資源(如I/O)。

二、Java執行緒的6個狀態

// Thread.State 原始碼
public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

2.1、NEW

  用New語句建立的執行緒物件處於新建狀態。此時還沒呼叫Thread例項的start()方法,此時它和其他的java物件一樣,僅僅在堆區中分配了記憶體。

public class ThreadState_Demo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {});
        Thread.State state = thread.getState();
        System.out.println("state = " + state);
    }
}
//輸出NEW

從上面可以看出,只是建立了執行緒而並沒有呼叫start()方法,此時執行緒處於NEW狀態。

關於start()的兩個引申問題

  1. 反覆呼叫同一個執行緒的start()方法是否可行?

  2. 假如一個執行緒執行完畢(此時處於TERMINATED狀態),再次呼叫這個執行緒的start()方法是否可行?

public class ThreadState_Demo {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {});
        Thread.State state 
= thread.getState(); System.out.println("state = " + state); thread.start(); Thread.sleep(5000); System.out.println(thread.getState()); thread.start(); } }

要分析這兩個問題,我們先來看看start()的原始碼:

public synchronized void start() {
    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) {
        }
    }
}

我們可以看到,在start()內部,這裡有一個threadStatus的變數。如果它不等於0,呼叫start()是會直接丟擲異常的。

我們接著往下看,有一個native的start0()方法。這個方法裡並沒有對threadStatus的處理。到了這裡我們彷彿就拿這個threadStatus沒轍了,我們通過debug的方式再看一下:

@Test
public void testStartMethod() {
    Thread thread = new Thread(() -> {});
    thread.start(); // 第一次呼叫
    thread.start(); // 第二次呼叫
}

我是在start()方法內部的最開始打的斷點,敘述下在我這裡打斷點看到的結果:

  • 第一次呼叫時threadStatus的值是0。

  • 第二次呼叫時threadStatus的值不為0。

檢視當前執行緒狀態的原始碼:

// Thread.getState方法原始碼:
public State getState() {
    // get current thread state
    return sun.misc.VM.toThreadState(threadStatus);
}

// sun.misc.VM 原始碼:
public static State toThreadState(int var0) {
    if ((var0 & 4) != 0) {
        return State.RUNNABLE;
    } else if ((var0 & 1024) != 0) {
        return State.BLOCKED;
    } else if ((var0 & 16) != 0) {
        return State.WAITING;
    } else if ((var0 & 32) != 0) {
        return State.TIMED_WAITING;
    } else if ((var0 & 2) != 0) {
        return State.TERMINATED;
    } else {
        return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
    }
}

所以,我們結合上面的原始碼可以得到引申的兩個問題的結果:

兩個問題的答案都是不可行,在呼叫一次start()之後,threadStatus的值會改變(threadStatus !=0),此時再次呼叫start()方法會丟擲IllegalThreadStateException異常。

比如,threadStatus為2代表當前執行緒狀態為TERMINATED。

2.2、RUNNABLE

  表示當前執行緒正在執行中。處於RUNNABLE狀態的執行緒在Java虛擬機器中執行,也有可能在等待CPU分配資源。此時,虛擬機器會為他建立方法呼叫棧和程式計數器。處於這種狀態的執行緒位於可執行池中,等待CPU的使用權。

Java中執行緒的RUNNABLE狀態

看了作業系統執行緒的幾個狀態之後我們來看看Thread原始碼裡對RUNNABLE狀態的定義:

2.3、BLOCKED

阻塞狀態。處於BLOCKED狀態的執行緒正等待鎖的釋放以進入同步區。

我們用BLOCKED狀態舉個生活中的例子:

假如今天你下班後準備去食堂吃飯。你來到食堂僅有的一個視窗,發現前面已經有個人在視窗前了,此時你必須得等前面的人從視窗離開才行。 假設你是執行緒t2,你前面的那個人是執行緒t1。此時t1佔有了鎖(食堂唯一的視窗),t2正在等待鎖的釋放,所以此時t2就處於BLOCKED狀態。

2.4、WAITING

等待狀態。處於等待狀態的執行緒變成RUNNABLE狀態需要其他執行緒喚醒。

呼叫如下3個方法會使執行緒進入等待狀態:

//使當前執行緒處於等待狀態直到另一個執行緒喚醒它;
Object.wait():
//等待執行緒執行完畢,底層呼叫的是Object例項的wait方法;
Thread.join():
//除非獲得呼叫許可,否則禁用當前執行緒進行執行緒排程。
LockSupport.park():

2.5、TIMED_WAITING

  超時等待狀態。執行緒等待一個具體的時間,時間到後會被自動喚醒。

呼叫如下方法會使執行緒進入超時等待狀態:

//使當前執行緒睡眠指定時間;
Thread.sleep(long millis)
//執行緒休眠指定時間,等待期間可以通過notify()/notifyAll()喚醒;
Object.wait(long timeout)
//等待當前執行緒最多執行millis毫秒,如果millis為0,則會一直執行;
Thread.join(long millis)
//除非獲得呼叫許可,否則禁用當前執行緒進行執行緒排程指定時間;
LockSupport.parkNanos(long nanos)
//同上,也是禁止執行緒進行排程指定時間;
LockSupport.parkUntil(long deadline)

我們繼續延續上面的例子來解釋一下TIMED_WAITING狀態:

到了第二天中午,又到了飯點,你還是到了視窗前。

突然間想起你的同事叫你等他一起,他說讓你等他十分鐘他改個bug。

好吧,你說那你就等等吧,你就離開了視窗。很快十分鐘過去了,你見他還沒來,你想都等了這麼久了還不來,那你還是先去吃飯好了。

這時你還是執行緒t1,你改bug的同事是執行緒t2。t2讓t1等待了指定時間,此時t1等待期間就屬於TIMED_WATING狀態。

t1等待10分鐘後,就自動喚醒,擁有了去爭奪鎖的資格。

2.6、TERMINATED

  終止狀態。此時執行緒已執行完畢。

三、執行緒狀態的轉換

  根據上面關於執行緒狀態的介紹我們可以得到下面的執行緒狀態轉換圖

3.1、BLOCKED與RUNNABLE狀態的轉換

BLOCKED狀態的執行緒是因為在等待鎖的釋放。假如這裡有兩個執行緒a和b,a執行緒提前獲得了鎖並且暫未釋放鎖,此時b就處於BLOCKED狀態。我們先來看一個例子:

@Test
public void blockedTest() {

    Thread a = new Thread(new Runnable() {
        @Override
        public void run() {
            testMethod();
        }
    }, "a");
    Thread b = new Thread(new Runnable() {
        @Override
        public void run() {
            testMethod();
        }
    }, "b");

    a.start();
    b.start();
    System.out.println(a.getName() + ":" + a.getState()); // 輸出?
    System.out.println(b.getName() + ":" + b.getState()); // 輸出?
}

// 同步方法爭奪鎖
private synchronized void testMethod() {
    try {
        Thread.sleep(2000L);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

初看之下,大家可能會覺得執行緒a會先呼叫同步方法,同步方法內又呼叫了Thread.sleep()方法,必然會輸出TIMED_WAITING,而執行緒b因為等待執行緒a釋放鎖所以必然會輸出BLOCKED。

其實不然,有兩點需要值得大家注意,一是在測試方法blockedTest()內還有一個main執行緒,二是啟動執行緒後執行run方法還是需要消耗一定時間的

測試方法的main執行緒只保證了a,b兩個執行緒呼叫start()方法(轉化為RUNNABLE狀態),如果CPU執行效率高一點,還沒等兩個執行緒真正開始爭奪鎖,就已經列印此時兩個執行緒的狀態(RUNNABLE)了。

當然,如果CPU執行效率低一點,其中某個執行緒也是可能打印出BLOCKED狀態的(此時兩個執行緒已經開始爭奪鎖了)。

這時你可能又會問了,要是我想要打印出BLOCKED狀態我該怎麼處理呢?BLOCKED狀態的產生需要兩個執行緒爭奪鎖才行。那我們處理下測試方法裡的main執行緒就可以了,讓它“休息一會兒”,呼叫一下Thread.sleep()方法。

這裡需要注意的是main執行緒休息的時間,要保證線上程爭奪鎖的時間內,不要等到前一個執行緒鎖都釋放了你再去爭奪鎖,此時還是得不到BLOCKED狀態的。

我們把上面的測試方法blockedTest()改動一下:

public void blockedTest() throws InterruptedException {
    ······
    a.start();
    Thread.sleep(1000L); // 需要注意這裡main執行緒休眠了1000毫秒,而testMethod()裡休眠了2000毫秒
    b.start();
    System.out.println(a.getName() + ":" + a.getState()); // 輸出?
    System.out.println(b.getName() + ":" + b.getState()); // 輸出?
}

在這個例子中兩個執行緒的狀態轉換如下

  • a的狀態轉換過程:RUNNABLE(a.start()) -> TIMED_WATING(Thread.sleep())->RUNABLE(sleep()時間到)->BLOCKED(未搶到鎖) -> TERMINATED

  • b的狀態轉換過程:RUNNABLE(b.start()) -> BLOCKED(未搶到鎖) ->TERMINATED

斜體表示可能出現的狀態, 大家可以在自己的電腦上多試幾次看看輸出。同樣,這裡的輸出也可能有多鍾結果。

3.2、WAITING狀態與RUNNABLE狀態的轉換

根據轉換圖我們知道有3個方法可以使執行緒從RUNNABLE狀態轉為WAITING狀態。我們主要介紹下Object.wait()Thread.join()

Object.wait()

呼叫wait()方法前執行緒必須持有物件的鎖。

執行緒呼叫wait()方法時,會釋放當前的鎖,直到有其他執行緒呼叫notify()/notifyAll()方法喚醒等待鎖的執行緒。

需要注意的是,其他執行緒呼叫notify()方法只會喚醒單個等待鎖的執行緒,如有有多個執行緒都在等待這個鎖的話不一定會喚醒到之前呼叫wait()方法的執行緒。

同樣,呼叫notifyAll()方法喚醒所有等待鎖的執行緒之後,也不一定會馬上把時間片分給剛才放棄鎖的那個執行緒,具體要看系統的排程。

Thread.join()

呼叫join()方法不會釋放鎖,會一直等待當前執行緒執行完畢(轉換為TERMINATED狀態)。

我們再把上面的例子執行緒啟動那裡改變一下:

public void blockedTest() {
    ······
    a.start();
    a.join();
    b.start();
    System.out.println(a.getName() + ":" + a.getState()); // 輸出 TERMINATED
    System.out.println(b.getName() + ":" + b.getState());
}

要是沒有呼叫join方法,main執行緒不管a執行緒是否執行完畢都會繼續往下走。

a執行緒啟動之後馬上呼叫了join方法,這裡main執行緒就會等到a執行緒執行完畢,所以這裡a執行緒列印的狀態固定是TERMINATED

至於b執行緒的狀態,有可能列印RUNNABLE(尚未進入同步方法),也有可能列印TIMED_WAITING(進入了同步方法)。

3.3、TIMED_WAITING與RUNNABLE狀態轉換

TIMED_WAITING與WAITING狀態類似,只是TIMED_WAITING狀態等待的時間是指定的。

Thread.sleep(long)

使當前執行緒睡眠指定時間。需要注意這裡的“睡眠”只是暫時使執行緒停止執行,並不會釋放鎖。時間到後,執行緒會重新進入RUNNABLE狀態。

Object.wait(long)

wait(long)方法使執行緒進入TIMED_WAITING狀態。這裡的wait(long)方法與無參方法wait()相同的地方是,都可以通過其他執行緒呼叫notify()或notifyAll()方法來喚醒。

不同的地方是,有參方法wait(long)就算其他執行緒不來喚醒它,經過指定時間long之後它會自動喚醒,擁有去爭奪鎖的資格。

Thread.join(long)

join(long)使當前執行緒執行指定時間,並且使執行緒進入TIMED_WAITING狀態。

我們再來改一改剛才的示例:

public void blockedTest() {
 ······
 a.start();
 a.join(1000L);
 b.start();
 System.out.println(a.getName() + ":" + a.getState()); // 輸出 TIEMD_WAITING
 System.out.println(b.getName() + ":" + b.getState());
}

這裡呼叫a.join(1000L),因為是指定了具體a執行緒執行的時間的,並且執行時間是小於a執行緒sleep的時間,所以a執行緒狀態輸出TIMED_WAITING。

b執行緒狀態仍然不固定(RUNNABLE或BLOCKED)。

3.4、執行緒中斷

在某些情況下,我們線上程啟動後發現並不需要它繼續執行下去時,需要中斷執行緒。目前在Java裡還沒有安全直接的方法來停止執行緒,但是Java提供了執行緒中斷機制來處理需要中斷執行緒的情況。

執行緒中斷機制是一種協作機制。需要注意,通過中斷操作並不能直接終止一個執行緒,而是通知需要被中斷的執行緒自行處理。

簡單介紹下Thread類裡提供的關於執行緒中斷的幾個方法:

//中斷執行緒。這裡的中斷執行緒並不會立即停止執行緒,而是設定執行緒的中斷狀態為true(預設是flase);
Thread.interrupt()
//測試當前執行緒是否被中斷。執行緒的中斷狀態受這個方法的影響,意思是呼叫一次使執行緒中斷狀態設定為true,連續呼叫兩次會使得這個執行緒的中斷狀態重新轉為false;
Thread.interrupted()
//測試當前執行緒是否被中斷。與上面方法不同的是呼叫這個方法並不會影響執行緒的中斷狀態。
Thread.isInterrupted()

線上程中斷機制裡,當其他執行緒通知需要被中斷的執行緒後,執行緒中斷的狀態被設定為true,但是具體被要求中斷的執行緒要怎麼處理,完全由被中斷執行緒自己而定,可以在合適的實際處理中斷請求,也可以完全不處理繼續執行下去。