漫談Java線程狀態

前言

Java語言定義了 6 種線程狀態,在任意一個時間點中,一個線程只能只且只有其中的一種狀態,並且可以通過特定的方法在不同狀態之間進行轉換。

今天,我們就詳細聊聊這幾種狀態,以及在什麼情況下會發生轉換。

一、線程狀態

要想知道Java線程都有哪些狀態,我們可以直接來看 Thread,它有一個枚舉類 State

public class Thread {

    public enum State {

        /**
         * 新建狀態
         * 創建後尚未啓動的線程
         */
        NEW,

        /**
         * 運行狀態
         * 包括正在執行,也可能正在等待操作系統爲它分配執行時間
         */
        RUNNABLE,

        /**
         * 阻塞狀態
         * 一個線程因爲等待臨界區的鎖被阻塞產生的狀態
         */
        BLOCKED,

        /**
         * 無限期等待狀態
         * 線程不會被分配處理器執行時間,需要等待其他線程顯式喚醒
         */
        WAITING,

        /**
         * 限期等待狀態
         * 線程不會被分配處理器執行時間,但也無需等待被其他線程顯式喚醒
         * 在一定時間之後,它們會由操作系統自動喚醒
         */
        TIMED_WAITING,

        /**
         * 結束狀態
         * 線程退出或已經執行完成
         */
        TERMINATED;
    }
}

二、狀態轉換

我們說,線程狀態並非是一成不變的,可以通過特定的方法在不同狀態之間進行轉換。那麼接下來,我們通過代碼,具體來看看這些個狀態是怎麼形成的。

1、新建

新建狀態最爲簡單,創建一個線程後,尚未啓動的時候就處於此種狀態。

public static void main(String[] args) {
    Thread thread = new Thread("新建線程");
    System.out.println("線程狀態:"+thread.getState());
}
-- 輸出:線程狀態:NEW

2、運行

可運行線程的狀態,當我們調用了start()方法,線程正在Java虛擬機中執行,但它可能正在等待來自操作系統(如處理器)的其他資源。

所以,這裏實際上包含了兩種狀態:Running 和 Ready,統稱爲 Runnable。這是爲什麼呢?

這裏涉及到一個Java線程調度的問題:

線程調度,是指系統爲線程分配處理器使用權的過程。調度主要方式有兩種,協同式線程調度和搶佔式線程調度。

  • 協同式線程調度

線程的執行時間由線程本身來控制,線程把自己的工作執行完畢之後,要主動通知系統切換到另外一個線程上去。

  • 搶佔式線程調度

每個線程將由系統來自動分配執行時間,線程的切換不由線程本身來決定,是基於CPU時間分片的方式。

它們孰優孰劣,不在本文討論範圍之內。我們只需要知道,Java使用的線程調度方式就是搶佔式調度。

通常,這個時間分片是很小的,可能只有幾毫秒或幾十毫秒。所以,線程的實際狀態可能會在Running 和 Ready狀態之間不斷變化。所以,再去區分它們意義不大。

那麼,我們再多想一下,如果Java線程調度方式是協同式調度,也許再去區分這兩個狀態就很有必要了。

public static void main(String[] args) {
    
    Thread thread = new Thread(() -> {
        for (;;){}
    });
    thread.start();
    System.out.println("線程狀態:"+thread.getState());
}
-- 輸出:線程狀態:RUNNABLE

簡單來看,上面的代碼就使線程處於Runnable狀態。但值得我們注意的是,如果一個線程在等待阻塞I/O的操作時,它的狀態也是Runnable的。

我們來看兩個經典阻塞IO的例子:

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

    Thread t1 = new Thread(() -> {
        try {
            ServerSocket serverSocket = new ServerSocket(9999);
            while (true){
                Socket socket = serverSocket.accept();
                OutputStream outputStream = socket.getOutputStream();
                outputStream.write("Hello".getBytes());
                outputStream.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    },"accept");
    t1.start();

    Thread t2 = new Thread(() -> {
        try {
            Socket socket = new Socket("127.0.0.1",9999);
            for (;;){
                InputStream inputStream = socket.getInputStream();
                byte[] bytes = new byte[5];
                inputStream.read(bytes);
                System.out.println(new String(bytes));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    },"read");
    t2.start();
}

上面的代碼中,我們知道,serverSocket.accept()inputStream.read(bytes);都是阻塞式方法。

它們一個在等待客戶端的連接;一個在等待數據的到來。但是,這兩個線程的狀態卻是 RUNNABLE的。

"read" #13 prio=5 os_prio=0 tid=0x0000000023f6c800 nid=0x1cd0 runnable [0x0000000024b3e000]
   java.lang.Thread.State: RUNNABLE
    at java.net.SocketInputStream.socketRead0(Native Method)
    at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
    at java.net.SocketInputStream.read(SocketInputStream.java:171)
    at java.net.SocketInputStream.read(SocketInputStream.java:141)
"accept" #12 prio=5 os_prio=0 tid=0x0000000023f68000 nid=0x4cec runnable [0x0000000024a3e000]
   java.lang.Thread.State: RUNNABLE
    at java.net.DualStackPlainSocketImpl.accept0(Native Method)
    at java.net.DualStackPlainSocketImpl.socketAccept(DualStackPlainSocketImpl.java:131)
    at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409)
    at java.net.PlainSocketImpl.accept(PlainSocketImpl.java:199)

這又是爲什麼呢 ?

我們前面說過,處於 Runnable 狀態下的線程,正在 Java 虛擬機中執行,但它可能正在等待來自操作系統(如處理器)的其他資源

不管是CPU、網卡還是硬盤,這些都是操作系統的資源而已。當進行阻塞式的IO操作時,或許底層的操作系統線程確實處在阻塞狀態,但在這裏我們的 Java 虛擬機線程的狀態還是 Runnable

不要小看這個問題,很具有迷惑性。有些面試官如果問到,如果一個線程正在進行阻塞式 I/O 操作時,它處於什麼狀態?是Blocked還是Waiting?

那這時候,我們就要義正言辭的告訴他:親,都不是哦~

3、無限期等待

處於無限期等待狀態下的線程,不會被分配處理器執行時間,除非其他線程顯式的喚醒它。

最簡單的場景就是調用了 Object.wait() 方法。

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

    Object object = new Object();
    new Thread(() -> {
        synchronized (object){
        try {
            object.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }}).start();
}
-- 輸出:線程狀態:WAITING

此時這個線程就處於無限期等待狀態,除非有別的線程顯式的調用object.notifyAll();來喚醒它。

然後,就是Thread.join()方法,當主線程調用了此方法,就必須等待子線程結束之後才能繼續進行。

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

    Thread mainThread = new Thread(() -> {
        Thread subThread = new Thread(() -> {
            for (;;){}
        });
        subThread.start();
        try {
            subThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    mainThread.start();
    System.out.println("線程狀態:"+thread.getState());
}
//輸出:線程狀態:WAITING

如上代碼,在主線程 mainThread 中調用了子線程的join()方法,那麼主線程就要等待子線程結束運行。所以此時主線程mainThread的狀態就是無限期等待。
多說一句,其實join()方法內部,調用的也是Object.wait()

最後,我們說說LockSupport.park()方法,它同樣會使線程進入無限期等待狀態。也許有的朋友對它很陌生,沒有用過,我們來看一個阻塞隊列的例子。

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

    ArrayBlockingQueue<Long> queue = new ArrayBlockingQueue(1);
    Thread thread = new Thread(() -> {
        while (true){
            try {
                queue.put(System.currentTimeMillis());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });
    thread.start();
}

如上代碼,往往我們會通過阻塞隊列的方式來做生產者-消費者模型的代碼。

這裏,ArrayBlockingQueue長度爲1,當我們第二次往裏面添加數據的時候,發現隊列已滿,線程就會等待這裏,它的源碼裏面正是調用了LockSupport.park()

同樣的,這裏也比較具有迷惑性,我來問你:阻塞隊列中,如果隊列爲空或者隊列已滿,這時候執行take或者put操作的時候,線程的狀態是 Blocked 嗎?

那這時候,我們需要謹記這裏的線程狀態還是 WAITING。它們之間的區別和聯繫,我們後文再看。

4、限期等待

同樣的,處於限期等待狀態下的線程,也不會被分配處理器執行時間,但是它在一定時間之後可以自動的被操作系統喚醒。

這個跟無限期等待的區別,僅僅就是有沒有帶有超時時間參數。

比如:

object.wait(3000);
thread.join(3000);
LockSupport.parkNanos(5000000L);
Thread.sleep(1000);

像這種操作,都會使線程處於限期等待的狀態 TIMED_WAITING。因爲Thread.sleep()必須帶有時間參數,所以它不在無限期等待行列中。

5、阻塞

一個線程因爲等待臨界區的鎖被阻塞產生的狀態,也就是說,阻塞狀態的產生是因爲它正在等待着獲取一個排它鎖。

這裏,我們來看一個 synchronized的例子。

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

    Object object = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (object){
            for (;;){}
        }
    });
    t1.start();

    Thread t2 = new Thread(() -> {
        synchronized (object){
            System.out.println("獲取到object鎖,線程執行。");
        }
    });
    t2.start();
    System.out.println("線程狀態:"+t2.getState());
}
//輸出:線程狀態:BLOCKED

我們看上面的代碼,object對象鎖一直被線程 t1 持有,所以線程 t2 的狀態一直會是阻塞狀態。

我們接着再來看一個鎖的例子:

public static void main(String[] args){

    Lock lock = new ReentrantLock();
    lock.lock();
    Thread t1 = new Thread(() -> {
        lock.lock();
        System.out.println("已獲取lock鎖,線程執行");
        lock.unlock();
    });
    t1.start();
    System.out.println("線程狀態:"+t1.getState());
}

如上代碼,我們有一個ReentrantLock,main線程已經持有了這個鎖,t1 線程會一直等待在lock.lock();

那麼,此時 t1 線程的狀態是什麼呢 ?

其實答案是WAITING,即無限期等待狀態。這又是爲什麼呢 ?

原因在於,Lock接口是Java API實現的鎖,它的底層實現其實是抽象同步隊列,簡稱AQS

在通過lock.lock()獲取鎖的時候,如果鎖正在被其他線程持有,那麼線程會被放入AQS隊列後,阻塞掛起。

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        如果tryAcquire返回false,會把當前線程放入AQS阻塞隊列
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

acquireQueued方法會將當前線程放入 AQS 阻塞隊列,然後調用LockSupport.park(this);掛起線程。

所以,這也就解釋了爲什麼lock.lock()獲取鎖的時候,當前的線程狀態會是 WAITING

常常有人會問,synchronized和Lock的區別,除了一般性的答案,此時你也可以說一下線程狀態的差異,我猜可能很少有人會意識到這一點。

6、結束

一個線程,當它退出或已經執行完成的時候,就是結束狀態。

public static void main(String[] args) throws Exception {
    
    Thread thread = new Thread(() -> System.out.println("線程已執行"));
    thread.start();
    Thread.sleep(1000);
    System.out.println("線程狀態:"+thread.getState());
}
//輸出:  線程已執行
線程狀態:TERMINATED

三、總結

本文介紹了 Java 線程的不同狀態,以及在何種情況下發生轉換。

原創不易,客官們點個贊再走嘛,這將是筆者持續寫作的動力~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章