4-Thread類詳解與執行緒的狀態分析

weixin_33724059發表於2018-02-04

Thread類詳解與執行緒的狀態分析

執行緒的狀態

在正式學習Thread類中的具體方法之前,我們先來了解一下執行緒有哪些狀態,這個將會有助於後面對Thread類中的方法的理解。

執行緒從建立到最終的消亡,要經歷若干個狀態。一般來說,執行緒包括以下這幾個狀態:建立(new)、就緒(runnable)、執行(running)、阻塞(blocked)、time waiting、waiting、消亡(dead)。

當需要新起一個執行緒來執行某個子任務時,就建立了一個執行緒。但是執行緒建立之後,不會立即進入就緒狀態,因為執行緒的執行需要一些條件(比如記憶體資源:程式計數器、Java棧、本地方法棧都是執行緒私有的,所以需要為執行緒分配一定的記憶體空間),只有執行緒執行需要的所有條件滿足了,才進入就緒狀態。

當執行緒進入就緒狀態後,不代表立刻就能獲取CPU執行時間,也許此時CPU正在執行其他的事情,因此它要等待。當得到CPU執行時間之後,執行緒便真正進入執行狀態。

執行緒在執行狀態過程中,可能有多個原因導致當前執行緒不繼續執行下去,比如使用者主動讓執行緒睡眠(睡眠一定的時間之後再重新執行)、使用者主動讓執行緒等待,或者被同步塊給阻塞,此時就對應著多個狀態:time waiting(睡眠或等待一定的時間)、waiting(等待被喚醒)、blocked(阻塞)。

當由於突然中斷或者子任務執行完畢,執行緒就會被消亡。

下面這副圖描述了執行緒從建立到消亡之間的狀態:

2834970-768c5ce258230e7f.png
image.png

在有些教程上將blocked、waiting、time waiting統稱為阻塞狀態,這個也是可以的,只不過這裡我想將執行緒的狀態和Java中的方法呼叫聯絡起來,所以將waiting和time waiting兩個狀態分離出來。

上下文切換

對於單核CPU來說(對於多核CPU,此處就理解為一個核),CPU在一個時刻只能執行一個執行緒,當在執行一個執行緒的過程中轉去執行另外一個執行緒,這個叫做執行緒上下文切換(對於程式也是類似)。

由於可能當前執行緒的任務並沒有執行完畢,所以在切換時需要儲存執行緒的執行狀態,以便下次重新切換回來時能夠繼續切換之前的狀態執行。舉個簡單的例子:比如一個執行緒A正在讀取一個檔案的內容,正讀到檔案的一半,此時需要暫停執行緒A,轉去執行執行緒B,當再次切換回來執行執行緒A的時候,我們不希望執行緒A又從檔案的開頭來讀取。

因此需要記錄執行緒A的執行狀態,那麼會記錄哪些資料呢?因為下次恢復時需要知道在這之前當前執行緒已經執行到哪條指令了,所以需要記錄程式計數器的值,另外比如說執行緒正在進行某個計算的時候被掛起了,那麼下次繼續執行的時候需要知道之前掛起時變數的值時多少,因此需要記錄CPU暫存器的狀態。所以一般來說,執行緒上下文切換過程中會記錄程式計數器、CPU暫存器狀態等資料。

說簡單點的,對於執行緒的上下文切換實際上就是:儲存和恢復CPU狀態的過程,它使得執行緒執行能夠從中斷點恢復執行。

雖然多執行緒可以使得任務執行的效率得到提升,但是由於線上程切換時同樣會帶來一定的開銷代價,並且多個執行緒會導致系統資源佔用的增加,所以在進行多執行緒程式設計時要注意這些因素。

Thread類中的方法

通過檢視java.lang.Thread類的原始碼可知:

public class Thread implements Runnable {
    /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }

    private char        name[];
    private int         priority;
    private Thread      threadQ;
    private long        eetop;

    /* Whether or not to single_step this thread. */
    private boolean     single_step;

    /* Whether or not the thread is a daemon thread. */
    private boolean     daemon = false;

    /* JVM state */
    private boolean     stillborn = false;

    /* What will be run. */
    private Runnable target;

    /* The group of this thread */
    private ThreadGroup group;

    /* The context ClassLoader for this thread */
    private ClassLoader contextClassLoader;

    /* The inherited AccessControlContext of this thread */
    private AccessControlContext inheritedAccessControlContext;
    ...
}

Thread類實現了Runnable介面,在Thread類中,有一些比較關鍵的屬性,比如name是表示Thread的名字,可以通過Thread類的構造器中的引數來指定執行緒名字,priority表示執行緒的優先順序(最大值為10,最小值為1,預設值為5),daemon表示執行緒是否是守護執行緒,target表示要執行的任務。

以下是關係到執行緒執行狀態的幾個方法:

  • start方法

start()用來啟動一個執行緒,當呼叫start方法後,系統才會開啟一個新的執行緒來執行使用者定義的子任務,在這個過程中,會為相應的執行緒分配需要的資源。

  • run方法

run()方法是不需要使用者來呼叫的,當通過start方法啟動一個執行緒之後,當執行緒獲得了CPU執行時間,便進入run方法體去執行具體的任務。注意,繼承Thread類必須重寫run方法,在run方法中定義具體要執行的任務。

  • sleep方法

sleep方法有兩個過載版本:

sleep(long millis)     //引數為毫秒
 
sleep(long millis, int nanoseconds)    //第一引數為毫秒,第二個引數為納秒

sleep相當於讓執行緒睡眠,交出CPU,讓CPU去執行其他的任務。

但是有一點要非常注意,sleep方法不會釋放鎖,也就是說如果當前執行緒持有對某個物件的鎖,則即使呼叫sleep方法,其他執行緒也無法訪問這個物件。看下面這個例子就清楚了:

public class Test {
     
    private int i = 10;
    private Object object = new Object();
     
    public static void main(String[] args) throws IOException  {
        Test test = new Test();
        MyThread thread1 = test.new MyThread();
        MyThread thread2 = test.new MyThread();
        thread1.start();
        thread2.start();
    } 
     
     
    class MyThread extends Thread{
        @Override
        public void run() {
            synchronized (object) {
                i++;
                System.out.println("i:"+i);
                try {
                    System.out.println("執行緒"+Thread.currentThread().getName()+"進入睡眠狀態");
                    Thread.currentThread().sleep(10000);
                } catch (InterruptedException e) {
                    // TODO: handle exception
                }
                System.out.println("執行緒"+Thread.currentThread().getName()+"睡眠結束");
                i++;
                System.out.println("i:"+i);
            }
        }
    }
}

輸出結果:

2834970-56a4ac4ea274affc.png
image.png

從上面輸出結果可以看出,當Thread-0進入睡眠狀態之後,Thread-1並沒有去執行具體的任務。只有當Thread-0執行完之後,此時Thread-0釋放了物件鎖,Thread-1才開始執行。

注意,如果呼叫了sleep方法,必須捕獲InterruptedException異常或者將該異常向上層丟擲。當執行緒睡眠時間滿後,不一定會立即得到執行,因為此時可能CPU正在執行其他的任務。所以說呼叫sleep方法相當於讓執行緒進入阻塞狀態。

  • yield方法

呼叫yield方法會讓當前執行緒交出CPU許可權,讓CPU去執行其他的執行緒。它跟sleep方法類似,同樣不會釋放鎖。但是yield不能控制具體的交出CPU的時間,另外,yield方法只能讓擁有相同優先順序的執行緒有獲取CPU執行時間的機會。

注意,呼叫yield方法並不會讓執行緒進入阻塞狀態,而是讓執行緒重回就緒狀態,它只需要等待重新獲取CPU執行時間,這一點是和sleep方法不一樣的。

  • join方法

join方法有三個過載版本:

join()
join(long millis)     //引數為毫秒
join(long millis,int nanoseconds)    //第一引數為毫秒,第二個引數為納秒

假如在main執行緒中,呼叫thread.join方法,則main方法會等待thread執行緒執行完畢或者等待一定的時間。如果呼叫的是無參join方法,則等待thread執行完畢,如果呼叫的是指定了時間引數的join方法,則等待一定的時間。

看下面一個例子:

public class Test {
     
    public static void main(String[] args) throws IOException  {
        System.out.println("進入執行緒"+Thread.currentThread().getName());
        Test test = new Test();
        MyThread thread1 = test.new MyThread();
        thread1.start();
        try {
            System.out.println("執行緒"+Thread.currentThread().getName()+"等待");
            thread1.join();
            System.out.println("執行緒"+Thread.currentThread().getName()+"繼續執行");
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    } 
     
    class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println("進入執行緒"+Thread.currentThread().getName());
            try {
                Thread.currentThread().sleep(5000);
            } catch (InterruptedException e) {
                // TODO: handle exception
            }
            System.out.println("執行緒"+Thread.currentThread().getName()+"執行完畢");
        }
    }
}

輸出結果:

2834970-71dcb79eb9228b42.png
image.png

可以看出,當呼叫thread1.join()方法後,main執行緒會進入等待,然後等待thread1執行完之後再繼續執行。

實際上呼叫join方法是呼叫了Object的wait方法,這個可以通過檢視原始碼得知:

public final synchronized void join(long millis)
throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

wait方法會讓執行緒進入阻塞狀態,並且會釋放執行緒佔有的鎖,並交出CPU執行許可權。

由於wait方法會讓執行緒釋放物件鎖,所以join方法同樣會讓執行緒釋放對一個物件持有的鎖。具體的wait方法使用在後面文章中給出。

  • interrupt方法

interrupt,顧名思義,即中斷的意思。單獨呼叫interrupt方法可以使得處於阻塞狀態的執行緒丟擲一個異常,也就說,它可以用來中斷一個正處於阻塞狀態的執行緒;另外,通過interrupt方法和isInterrupted()方法來停止正在執行的執行緒。

下面看一個例子:

public class Test {
     
    public static void main(String[] args) throws IOException  {
        Test test = new Test();
        MyThread thread = test.new MyThread();
        thread.start();
        try {
            Thread.currentThread().sleep(2000);
        } catch (InterruptedException e) {
             
        }
        thread.interrupt();
    } 
     
    class MyThread extends Thread{
        @Override
        public void run() {
            try {
                System.out.println("進入睡眠狀態");
                Thread.currentThread().sleep(10000);
                System.out.println("睡眠完畢");
            } catch (InterruptedException e) {
                System.out.println("得到中斷異常");
            }
            System.out.println("run方法執行完畢");
        }
    }
}

輸出結果:

[圖片上傳失敗...(image-3e6a7-1517750888439)]

從這裡可以看出,通過interrupt方法可以中斷處於阻塞狀態的執行緒。那麼能不能中斷處於非阻塞狀態的執行緒呢?看下面這個例子:

public class Test {
    
    public static void main(String[] args) throws IOException  {
        Test test = new Test();
        MyThread thread = test.new MyThread();
        thread.start();
        try {
            Thread.currentThread().sleep(2000);
        } catch (InterruptedException e) {
             
        }
        thread.interrupt();
    } 
    
    class MyThread extends Thread{
        @Override
        public void run() {
            int i = 0;
            while(i<Integer.MAX_VALUE){
                System.out.println(i+" while迴圈");
                i++;
            }
        }
    }
}

執行該程式會發現,while迴圈會一直執行直到變數i的值超出Integer.MAX_VALUE。所以說直接呼叫interrupt方法不能中斷正在執行中的執行緒。

但是如果配合isInterrupted()能夠中斷正在執行的執行緒,因為呼叫interrupt方法相當於將中斷標誌位置為true,那麼可以通過呼叫isInterrupted()判斷中斷標誌是否被置為true來中斷執行緒的執行。比如下面這段程式碼:

public class Test {
     
    public static void main(String[] args) throws IOException  {
        Test test = new Test();
        MyThread thread = test.new MyThread();
        thread.start();
        try {
            Thread.currentThread().sleep(2000);
        } catch (InterruptedException e) {
             
        }
        thread.interrupt();
    } 
     
    class MyThread extends Thread{
        @Override
        public void run() {
            int i = 0;
            while(!isInterrupted() && i<Integer.MAX_VALUE){
                System.out.println(i+" while迴圈");
                i++;
            }
        }
    }
}

執行會發現,列印若干個值之後,while迴圈就停止列印了。

但是一般情況下不建議通過這種方式來中斷執行緒,一般會在MyThread類中增加一個屬性 isStop來標誌是否結束while迴圈,然後再在while迴圈中判斷isStop的值。

class MyThread extends Thread{
    private volatile boolean isStop = false;
    @Override
    public void run() {
        int i = 0;
        while(!isStop){
            i++;
        }
    }
     
    public void setStop(boolean stop){
        this.isStop = stop;
    }
}

那麼就可以在外面通過呼叫setStop方法來終止while迴圈。

  • stop方法

stop方法已經是一個廢棄的方法,它是一個不安全的方法。因為呼叫stop方法會直接終止run方法的呼叫,並且會丟擲一個ThreadDeath錯誤,如果執行緒持有某個物件鎖的話,會完全釋放鎖,導致物件狀態不一致。所以stop方法基本是不會被用到的。

  • destroy方法

destroy方法也是廢棄的方法。基本不會被使用到。

以下是關係到執行緒屬性的幾個方法:

  • getId

用來得到執行緒ID

  • getName和setName

用來得到或者設定執行緒名稱。

  • getPriority和setPriority

用來獲取和設定執行緒優先順序。

  • setDaemon和isDaemon

用來設定執行緒是否成為守護執行緒和判斷執行緒是否是守護執行緒。

守護執行緒和使用者執行緒的區別在於:守護執行緒依賴於建立它的執行緒,而使用者執行緒則不依賴。舉個簡單的例子:如果在main執行緒中建立了一個守護執行緒,當main方法執行完畢之後,守護執行緒也會隨著消亡。而使用者執行緒則不會,使用者執行緒會一直執行直到其執行完畢。在JVM中,像垃圾收集器執行緒就是守護執行緒。

Thread類有一個比較常用的靜態方法currentThread()用來獲取當前執行緒。

在上面已經說到了Thread類中的大部分方法,那麼Thread類中的方法呼叫到底會引起執行緒狀態發生怎樣的變化呢?下面一幅圖就是在上面的圖上進行改進而來的:

2834970-dc6e23ef3796e83e.png
image.png

相關文章