漫談Java線程狀態(tài)

前言

Java語言定義了 6 種線程狀態(tài)植影,在任意一個時間點中涎永,一個線程只能只且只有其中的一種狀態(tài)羡微,并且可以通過特定的方法在不同狀態(tài)之間進行轉(zhuǎn)換。

今天博投,我們就詳細聊聊這幾種狀態(tài)盯蝴,以及在什么情況下會發(fā)生轉(zhuǎn)換捧挺。

一、線程狀態(tài)

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

public class Thread {

    public enum State {

        /**
         * 新建狀態(tài)
         * 創(chuàng)建后尚未啟動的線程
         */
        NEW,

        /**
         * 運行狀態(tài)
         * 包括正在執(zhí)行爬骤,也可能正在等待操作系統(tǒng)為它分配執(zhí)行時間
         */
        RUNNABLE,

        /**
         * 阻塞狀態(tài)
         * 一個線程因為等待臨界區(qū)的鎖被阻塞產(chǎn)生的狀態(tài)
         */
        BLOCKED,

        /**
         * 無限期等待狀態(tài)
         * 線程不會被分配處理器執(zhí)行時間莫换,需要等待其他線程顯式喚醒
         */
        WAITING,

        /**
         * 限期等待狀態(tài)
         * 線程不會被分配處理器執(zhí)行時間拉岁,但也無需等待被其他線程顯式喚醒
         * 在一定時間之后,它們會由操作系統(tǒng)自動喚醒
         */
        TIMED_WAITING,

        /**
         * 結(jié)束狀態(tài)
         * 線程退出或已經(jīng)執(zhí)行完成
         */
        TERMINATED;
    }
}

二惫企、狀態(tài)轉(zhuǎn)換

我們說陵叽,線程狀態(tài)并非是一成不變的巩掺,可以通過特定的方法在不同狀態(tài)之間進行轉(zhuǎn)換。那么接下來研儒,我們通過代碼独令,具體來看看這些個狀態(tài)是怎么形成的。

1逸月、新建

新建狀態(tài)最為簡單遍膜,創(chuàng)建一個線程后瓢颅,尚未啟動的時候就處于此種狀態(tài)。

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

2翰意、運行

可運行線程的狀態(tài),當我們調(diào)用了start()方法醒第,線程正在Java虛擬機中執(zhí)行稠曼,但它可能正在等待來自操作系統(tǒng)(如處理器)的其他資源客年。

所以,這里實際上包含了兩種狀態(tài):Running 和 Ready司恳,統(tǒng)稱為 Runnable绍傲。這是為什么呢唧取?

這里涉及到一個Java線程調(diào)度的問題:

線程調(diào)度,是指系統(tǒng)為線程分配處理器使用權(quán)的過程邢享。調(diào)度主要方式有兩種淡诗,協(xié)同式線程調(diào)度和搶占式線程調(diào)度韩容。

  • 協(xié)同式線程調(diào)度

線程的執(zhí)行時間由線程本身來控制,線程把自己的工作執(zhí)行完畢之后插爹,要主動通知系統(tǒng)切換到另外一個線程上去请梢。

  • 搶占式線程調(diào)度

每個線程將由系統(tǒng)來自動分配執(zhí)行時間毅弧,線程的切換不由線程本身來決定,是基于CPU時間分片的方式寸宵。

它們孰優(yōu)孰劣,不在本文討論范圍之內(nèi)巫员。我們只需要知道疏遏,Java使用的線程調(diào)度方式就是搶占式調(diào)度救军。

通常倘零,這個時間分片是很小的呈驶,可能只有幾毫秒或幾十毫秒。所以司致,線程的實際狀態(tài)可能會在Running 和 Ready狀態(tài)之間不斷變化脂矫。所以霉晕,再去區(qū)分它們意義不大。

那么拄轻,我們再多想一下伟葫,如果Java線程調(diào)度方式是協(xié)同式調(diào)度筏养,也許再去區(qū)分這兩個狀態(tài)就很有必要了撼玄。

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

簡單來看,上面的代碼就使線程處于Runnable狀態(tài)盏浙。但值得我們注意的是,如果一個線程在等待阻塞I/O的操作時竹海,它的狀態(tài)也是Runnable的丐黄。

我們來看兩個經(jīng)典阻塞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);都是阻塞式方法蕉斜。

它們一個在等待客戶端的連接;一個在等待數(shù)據(jù)的到來机错。但是父腕,這兩個線程的狀態(tài)卻是 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 狀態(tài)下的線程尖奔,正在 Java 虛擬機中執(zhí)行穷当,但它可能正在等待來自操作系統(tǒng)(如處理器)的其他資源馁菜。

不管是CPU、網(wǎng)卡還是硬盤峭火,這些都是操作系統(tǒng)的資源而已卖丸。當進行阻塞式的IO操作時纺且,或許底層的操作系統(tǒng)線程確實處在阻塞狀態(tài),但在這里我們的 Java 虛擬機線程的狀態(tài)還是 Runnable稍浆。

不要小看這個問題载碌,很具有迷惑性。有些面試官如果問到衅枫,如果一個線程正在進行阻塞式 I/O 操作時嫁艇,它處于什么狀態(tài)?是Blocked還是Waiting弦撩?

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

3孤钦、無限期等待

處于無限期等待狀態(tài)下的線程纯丸,不會被分配處理器執(zhí)行時間偏形,除非其他線程顯式的喚醒它。

最簡單的場景就是調(diào)用了 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();
}
-- 輸出:線程狀態(tài):WAITING

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

然后坠陈,就是Thread.join()方法萨惑,當主線程調(diào)用了此方法,就必須等待子線程結(jié)束之后才能繼續(xù)進行仇矾。

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("線程狀態(tài):"+thread.getState());
}
//輸出:線程狀態(tài):WAITING

如上代碼庸蔼,在主線程 mainThread 中調(diào)用了子線程的join()方法,那么主線程就要等待子線程結(jié)束運行贮匕。所以此時主線程mainThread的狀態(tài)就是無限期等待姐仅。
多說一句,其實join()方法內(nèi)部刻盐,調(diào)用的也是Object.wait()掏膏。

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

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();
}

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

這里悼做,ArrayBlockingQueue長度為1疯特,當我們第二次往里面添加數(shù)據(jù)的時候,發(fā)現(xiàn)隊列已滿肛走,線程就會等待這里漓雅,它的源碼里面正是調(diào)用了LockSupport.park()

同樣的朽色,這里也比較具有迷惑性邻吞,我來問你:阻塞隊列中,如果隊列為空或者隊列已滿葫男,這時候執(zhí)行take或者put操作的時候抱冷,線程的狀態(tài)是 Blocked 嗎?

那這時候梢褐,我們需要謹記這里的線程狀態(tài)還是 WAITING旺遮。它們之間的區(qū)別和聯(lián)系,我們后文再看盈咳。

4耿眉、限期等待

同樣的,處于限期等待狀態(tài)下的線程鱼响,也不會被分配處理器執(zhí)行時間鸣剪,但是它在一定時間之后可以自動的被操作系統(tǒng)喚醒。

這個跟無限期等待的區(qū)別丈积,僅僅就是有沒有帶有超時時間參數(shù)筐骇。

比如:

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

像這種操作,都會使線程處于限期等待的狀態(tài) TIMED_WAITING江滨。因為Thread.sleep()必須帶有時間參數(shù)铛纬,所以它不在無限期等待行列中。

5唬滑、阻塞

一個線程因為等待臨界區(qū)的鎖被阻塞產(chǎn)生的狀態(tài)饺鹃,也就是說,阻塞狀態(tài)的產(chǎn)生是因為它正在等待著獲取一個排它鎖间雀。

這里悔详,我們來看一個 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鎖惹挟,線程執(zhí)行茄螃。");
        }
    });
    t2.start();
    System.out.println("線程狀態(tài):"+t2.getState());
}
//輸出:線程狀態(tài):BLOCKED

我們看上面的代碼,object對象鎖一直被線程 t1 持有连锯,所以線程 t2 的狀態(tài)一直會是阻塞狀態(tài)归苍。

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

public static void main(String[] args){

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

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

那么吻氧,此時 t1 線程的狀態(tài)是什么呢 溺忧?

其實答案是WAITING,即無限期等待狀態(tài)盯孙。這又是為什么呢 鲁森?

原因在于,Lock接口是Java API實現(xiàn)的鎖振惰,它的底層實現(xiàn)其實是抽象同步隊列歌溉,簡稱AQS

在通過lock.lock()獲取鎖的時候骑晶,如果鎖正在被其他線程持有痛垛,那么線程會被放入AQS隊列后,阻塞掛起桶蛔。

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

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

所以,這也就解釋了為什么lock.lock()獲取鎖的時候抖剿,當前的線程狀態(tài)會是 WAITING朽寞。

常常有人會問,synchronized和Lock的區(qū)別斩郎,除了一般性的答案脑融,此時你也可以說一下線程狀態(tài)的差異,我猜可能很少有人會意識到這一點缩宜。

6肘迎、結(jié)束

一個線程,當它退出或已經(jīng)執(zhí)行完成的時候锻煌,就是結(jié)束狀態(tài)妓布。

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

三、總結(jié)

本文介紹了 Java 線程的不同狀態(tài)宋梧,以及在何種情況下發(fā)生轉(zhuǎn)換匣沼。

image

原創(chuàng)不易,客官們點個贊再走嘛捂龄,這將是筆者持續(xù)寫作的動力~

image

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末释涛,一起剝皮案震驚了整個濱河市加叁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌唇撬,老刑警劉巖它匕,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡密末,警方通過查閱死者的電腦和手機陨闹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來轮傍,“玉大人,你說我怎么就攤上這事首装〈匆梗” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵仙逻,是天一觀的道長驰吓。 經(jīng)常有香客問我,道長系奉,這世上最難降的妖魔是什么檬贰? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮缺亮,結(jié)果婚禮上翁涤,老公的妹妹穿的比我還像新娘。我一直安慰自己萌踱,他們只是感情好葵礼,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著并鸵,像睡著了一般鸳粉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上园担,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天届谈,我揣著相機與錄音,去河邊找鬼弯汰。 笑死艰山,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的咏闪。 我是一名探鬼主播程剥,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了织鲸?” 一聲冷哼從身側(cè)響起舔腾,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎搂擦,沒想到半個月后稳诚,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡瀑踢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年扳还,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片橱夭。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡氨距,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出棘劣,到底是詐尸還是另有隱情俏让,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布茬暇,位于F島的核電站首昔,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏糙俗。R本人自食惡果不足惜勒奇,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望巧骚。 院中可真熱鬧赊颠,春花似錦、人聲如沸劈彪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽粉臊。三九已至草添,卻和暖如春驶兜,著一層夾襖步出監(jiān)牢的瞬間扼仲,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工抄淑, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留屠凶,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓肆资,卻偏偏與公主長得像矗愧,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345