Java多次啟動一個線程究竟會發生什么?程序到底會不會崩?大部分程序員理解錯誤!!
今天給大家分享的面試題是:一個線程調用兩次start()方法會出現什么現象?這道面試題是一道關于多線程的基礎面試題,很多小伙伴對這個面試題不太了解,其實,如果你看過JDK中關于Thread類的源碼,那這道面試題對你來說就能過輕松應對了。
手寫RPC框架視頻錄制中,發布地址:https://space.bilibili.com/517638832/channel/collectiondetail?sid=4186280
今天,我們就一起來聊聊這道面試題,以及面試官問這道題的面試分析拓展知識。
優質回答
Java 的線程是不允許啟動兩次的,第二次調用必然會拋出 IllegalThreadStateException,這是一種運行時異常,表示非法的線程狀態異常。
關于線程生命周期的不同狀態,在 Java 5 以后,線程狀態被明確定義在其公共內部枚舉類型 java.lang.Thread.State 中,分別是:
- 新建(NEW),表示線程被創建出來還沒真正啟動的狀態,可以認為它是個 Java 內部狀態。
- 就緒(RUNNABLE),表示該線程已經在 JVM 中執行,當然由于執行需要計算資源,它可能是正在運行,也可能還在等待系統分配給它 CPU 片段,在就緒隊列里面排隊。
- 在其他一些分析中,會額外區分一種狀態 RUNNING,但是從 Java API 的角度,并不能表示出來。
- 阻塞(BLOCKED),這個狀態和我們前面兩講介紹的同步非常相關,阻塞表示線程在等待 Monitor lock。比如,線程試圖通過 synchronized 去獲取某個鎖,但是其他線程已經獨占了,那么當前線程就會處于阻塞狀態。
- 等待(WAITING),表示正在等待其他線程采取某些操作。一個常見的場景是類似生產者消費者模式,發現任務條件尚未滿足,就讓當前消費者線程等待(wait),另外的生產者線程去準備任務數據,然后通過類似 notify 等動作,通知消費線程可以繼續工作了。Thread.join() 也會令線程進入等待狀態。
- 計時等待(TIMED_WAIT),其進入條件和等待狀態類似,但是調用的是存在超時條件的方法,比如 wait 或 join 等方法的指定超時版本,如下面示例:
public final native void wait(long timeout) throws InterruptedException;
- 終止(TERMINATED),不管是意外退出還是正常執行結束,線程已經完成使命,終止運行,也有人把這個狀態叫作死亡。
在第二次調用 start() 方法的時候,線程可能處于終止或者其他(非 NEW)狀態,但是不論如何,都是不可以再次啟動的。
面試分析
今天的面試題看似簡單,實則是對面試者基礎知識的考察,很多大廠在面試時,很重視面試者對基礎知識的掌握程度,往往這些基礎知識是大家最容易忽視的。
知識拓展
線程的通用生命周期
線程在運行的過程中,會經歷幾種狀態之間的轉換,而線程在這幾種狀態之間的轉換流程,基本上就構成了線程的生命周期。本小節,就簡單介紹下線程的通用生命周期。
線程的通用生命周期總體上可以分為五種狀態,分別為:初始狀態、可運行狀態、運行狀態、休眠狀態和終止狀態
圖片
可以看出,線程的通用生命周期可以分為初始狀態、可運行狀態、運行狀態、休眠狀態和終止狀態五種狀態。
(1)初始狀態:初始狀態比較特殊,這種狀態屬于編程語言層面特有的狀態,處于初始狀態的線程只是線程在編程語言層面被創建了,但是在操作系統層面,并沒有真正的創建線程。
(2)可運行狀態:在操作系統層面,線程被真正的創建,并且可以分配CPU執行。
(3)運行狀態:處于運行狀態的線程已經獲取到CPU資源,正在運行。
(4)休眠狀態:線程正在等待某個事件的發生(例如等待I/O事件的完成),或者調用了一個阻塞的API正處于阻塞狀態(例如以阻塞的方式讀寫文件等),此時的線程處于休眠狀態。
(5)終止狀態:線程正常運行結束或者出現異常,就會進入終止狀態。
線程的通用生命周期中各狀態之間的轉換關系如下所示。
(1)初始狀態轉換成可運行狀態:處于初始狀態的線程,實際上并沒有在操作系統中創建對應的線程,當在操作系統中創建了對應的線程時,此時線程就會從初始狀態轉換成可運行狀態。
(2)可運行狀態轉換成運行狀態:如果操作系統中存在空閑的CPU資源,則操作系統會將空閑的CPU資源分配給一個處于可運行狀態的線程,處于可運行狀態的線程獲得CPU資源后就會轉換成運行狀態。也就是說,處于可運行狀態的線程,被操作系統調度獲取到CPU資源后,就會從可運行狀態轉換成運行狀態。
(3)運行狀態轉換成可運行狀態:當正在運行的線程CPU時間片用完時,就會從運行狀態轉換成可運行狀態。
(4)運行狀態轉換成休眠狀態:處于運行狀態的線程如果等待某個事件的發生(例如,等待I/O事件的完成),或者調用了一個阻塞的API(例如,以阻塞的方式讀寫文件等),此時處于運行狀態的線程就會釋放CPU的資源,從運行狀態轉換成休眠狀態。
(5)休眠狀態轉換成可運行狀態:如果處于休眠狀態的線程等待的事件已經發生(例如,等待的I/O事件已經完成),或者調用的阻塞API已經完成操作(例如,以阻塞的方式讀寫文件已經完成),則線程就會從休眠狀態轉換成可運行狀態。
(6)運行狀態轉換成終止狀態:處于運行狀態的線程正常運行結束,或者出現異常,就會從運行狀態轉換成終止狀態。處于終止狀態的線程,不會再轉換成其他的狀態,線程的生命周期也就結束了。
注意:在線程的通用生命周期中,只有處于運行狀態的線程可以直接轉換成終止狀態和休眠狀態,處于其他狀態的線程都不能直接轉換成終止狀態和休眠狀態。處于休眠狀態的線程只能直接轉換成可運行狀態,不能直接轉換成其他狀態。
Java中線程的生命周期
在Java中,線程的生命周期主要包括:初始化狀態、可運行狀態、阻塞狀態、等待狀態、超時等待狀態和終止狀態。其中,可運行狀態又包括運行狀態和就緒狀態。
圖片
可以看出,在Java的線程生命周期中,總體上包含初始化狀態、可運行狀態、等待狀態、超時等待狀態、阻塞狀態和終止狀態六種狀態。
(1)初始化狀態:線程在Java中被創建,但是還沒有調用線程對象的start()方法,也就是說,還沒有創建操作系統層面對應的線程。
(2)可運行狀態:Java線程生命周期中的可運行狀態,包含操作系統中線程的運行狀態和就緒狀態。
(3)等待狀態:處于等待狀態的線程需要等待其他線程對當前線程進行通知或者中斷等操作,從而進入下一個線程狀態。
(4)超時等待狀態:處于超時等待狀態的線程需要在指定的時間內,等待其他線程對當前線程進行通知或者中斷等操作。如果在指定的時間內,存在其他線程對當前線程進行通知或者中斷等操作,則當前線程進入下一個狀態。否則超過指定的時間,當前線程也會進入下一個狀態。
(5)阻塞狀態:處于阻塞狀態的線程需要等待其他線程釋放鎖,或者等待進入synchronized臨界區。
(6)終止狀態:表示當前線程執行完畢,包括正常執行結束和異常退出。
Java的線程生命周期中的可運行狀態,涵蓋了運行狀態和就緒狀態。
(1)運行狀態:對應操作系統中的運行狀態。
(2)就緒狀態:對應操作系統中的就緒狀態。
在Java的線程生命周期中,各狀態之間的轉換關系如下所示。
1.初始化狀態轉換成可運行狀態的場景
在Java層面,調用線程對象的start()方法,會在操作系統層面創建對應的線程,此時,線程的狀態就會從初始化狀態轉換成可運行狀態。
2.可運行狀態與等待狀態互相轉換的場景一
(1)線程a調用synchronized(obj)獲取到對象鎖后,調用obj.wait()方法時,線程a的狀態會從可運行狀態轉換成等待狀態。
(2)在滿足(1)時,此時線程b調用synchronized(obj)獲取到對象鎖后,調用obj.notify()方法、obj.notifyAll()方法、a.interrupt()方法,此時會有兩種情況,如下所示。
l 線程a競爭鎖成功,則線程a會由等待狀態轉換成可運行狀態。
l 線程a競爭鎖失敗,則線程a會由等待狀態轉換成阻塞狀態。
3.可運行狀態與等待狀態互相轉換的場景二
(1)線程a調用線程b的join()方法時,線程a會由可運行狀態轉換成等待狀態。
(2)在滿足(1)時,線程b運行結束,或者調用了線程a的interrupt()方法,則線程a會從等待狀態轉換成可運行狀態。
4.可運行狀態與等待狀態互相轉換的場景三
(1)線程a調用LockSupport.park()方法時,線程a會從可運行狀態轉換成等待狀態。
(2)在滿足(1)時,其他線程調用LockSupport.unpark(a),或者調用線程a的interrupt()方法,線程a會從等待狀態轉換成可運行狀態。
5.可運行狀態與超時等待狀態互相轉換的場景一
(1)線程a調用synchronized(obj)獲取到對象鎖后,調用obj.wait(long n)方法,則線程a會從可運行狀態轉換成超時等待狀態。
(2)在滿足(1)時,線程a的等待時間超過了n毫秒,或者線程b調用synchronized(obj)獲取到對象鎖后,調用obj.notify()方法、obj.notifyAll()方法、a.interrupt()方法,此時會有兩種情況,如下所示。
l 線程a競爭鎖成功,則線程a會由超時等待狀態轉換成可運行狀態。
l 線程a競爭鎖失敗,則線程a會由超時等待狀態轉換成阻塞狀態。
6.可運行狀態與超時等待狀態互相轉換的場景二
(1)線程a調用Thread.sleep(long n)方法,則線程a會從可運行狀態轉換成超時等待狀態。
(2)在滿足(1)時,線程a的等待時間超過n毫秒,則線程a會從超時等待狀態轉換成可運行狀態。
7.可運行狀態與超時等待狀態互相轉換的場景三
(1)線程a調用了線程b的join(long n)方法時,線程a會從可運行狀態轉換成超時等待狀態。
(2)在滿足(1)時,線程a的等待時間超過n毫秒,或者線程b運行結束,或者調用了線程a的interrupt()方法,線程a會從超時等待狀態轉換成可運行狀態。
8.可運行狀態與超時等待狀態互相轉換的場景四
(1)線程a調用Locksupport.parkNanos(long nacos)方法,或者調用LockSupport.parkUntil(long millis)方法時,線程a會從可運行狀態轉換成超時等待狀態。
(2)在滿足(1)時,其他線程調用LockSupport.unpark(a),或者調用線程a的interrupt()方法,或者線程a等待超時,則線程a會從超時等待狀態轉換成可運行狀態。
9.可運行狀態與阻塞狀態互相轉換的場景一
(1)線程a與線程b共同爭搶同一個悲觀鎖,線程b爭搶成功,則線程a會從可運行狀態轉換成阻塞狀態。
(2)在滿足(1)時,線程b釋放鎖時,線程a獲取到鎖,則線程a會從阻塞狀態轉換成可運行狀態。
10.可運行狀態與阻塞狀態互相轉換的場景二
(1)線程a調用synchronized(obj)獲取對象鎖時,競爭失敗,則線程a會從可運行狀態轉換成阻塞狀態。
(2)在滿足(1)時,調用synchronized(obj)獲取對象鎖時競爭成功的線程,執行同步代碼塊完畢,就會喚醒所有阻塞在obj對象上的線程,這些被喚醒的線程會重新競爭,如果線程a競爭成功,則線程a會從阻塞狀態轉換成可運行狀態。如果線程a競爭失敗,則線程a繼續保持阻塞狀態。
11.可運行狀態轉換成終止狀態的場景
線程a正常執行結束,或者由于某種原因異常退出,線程a就會從可運行狀態轉換成終止狀態。
如果一個線程轉換成終止狀態,那么就標注著這個線程已經運行結束,不能再次轉換成其他狀態。