漫談Java線程狀態

前言

Java語言定義了 6 種線程狀態,在任意一個時間點中,一個線程只能只且只有其中的一種狀態,而且能夠經過特定的方法在不一樣狀態之間進行轉換。java

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

1、線程狀態

要想知道Java線程都有哪些狀態,咱們能夠直接來看 Thread,它有一個枚舉類 Statebash

public class Thread {

    public enum State {

        /**
         * 新建狀態
         * 建立後還沒有啓動的線程
         */
        NEW,

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

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

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

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

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

2、狀態轉換

咱們說,線程狀態並不是是一成不變的,能夠經過特定的方法在不一樣狀態之間進行轉換。那麼接下來,咱們經過代碼,具體來看看這些個狀態是怎麼造成的。socket

一、新建

新建狀態最爲簡單,建立一個線程後,還沒有啓動的時候就處於此種狀態。ui

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

二、運行

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

因此,這裏實際上包含了兩種狀態:Running 和 Ready,統稱爲 Runnable。這是爲何呢?spa

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

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

  • 協同式線程調度

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

  • 搶佔式線程調度

每一個線程將由系統來自動分配執行時間,線程的切換不禁線程自己來決定,是基於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?

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

三、無限期等待

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

最簡單的場景就是調用了 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。它們之間的區別和聯繫,咱們後文再看。

四、限期等待

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

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

好比:

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

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

五、阻塞

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

這裏,咱們來看一個 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的區別,除了通常性的答案,此時你也能夠說一下線程狀態的差別,我猜可能不多有人會意識到這一點。

六、結束

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

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
複製代碼

3、總結

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

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

相關文章
相關標籤/搜索