Java併發學習之線程狀態及Thread經常使用方法詳解

線程狀態及Thread經常使用方法詳解

I. 線程狀態

在前面線程建立的一篇博文中,明確說明只有在調用 Thread#start()方法以後,線程纔會啓動;那線程建立完和這個啓動又是什麼關係呢?啓動是否又是運行呢?本節則主要集中在線程的各個狀態的解釋以及狀態變遷的緣由java

先來一個圖,說明下線程的五個狀態編程

線程狀態變遷圖

1. 建立

顧名思義,就是建立了一個線程,也就經過 new Thread() 觸發併發

2. 就緒狀態

就緒,表示線程已經準備好了,隨時能夠進入運行,編程語言

start() 調用以後,線程進入就緒狀態,這個時候是準備運行,可是並無執行學習

3. 運行狀態

表示線程在執行了,真正工做跑任務線程

4. 阻塞狀態

線程運行以後,發生了一些變故,須要掛起時,這時就進入阻塞,把cpu和資源讓給其餘的線程去執行;這個就是阻塞狀態了code

也就是說,必須是有運行狀態進入阻塞狀態對象

5. 結束

線程執行完了,也是時候收拾收拾,各回各家了,就表示這個線程該乾的活幹完了,到過河拆橋的時候了,趕忙把這個線程丟到垃圾堆吧(線程回收),這個狀態就是線程結束(或者說線程死亡狀態)繼承


上面說了五個線程狀態,各是什麼意思,下面簡單說下他們的關係隊列

以一個線程的使用流程爲例

LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>();
// 建立一個線程
Thread thread = new Thread(() -> {
    try {
        System.out.println(queue.take());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
// 啓動線程
thread.start();
// 主線程掛起,保證thread線程邏輯進入並執行
Thread.sleep(2000);
// 主線程向隊列中塞一個數據,喚醒thread線程
queue.put("hello world");
// 等待線程執行完畢
thread.join();
// 線程執行結束
System.out.println("---over---");

建立線程有四種方式,能夠參考 《Java併發學習之四種線程建立方式的實現與對比》,

結合上面的case,分析下五種狀態的轉換過程:

  1. 首先是經過new來建立一個線程對象 thread, 這兒時候,線程就處於建立狀態了
  2. 接着我須要線程工做了,而後調用thread.start()方法,來啓動線程
  • 這個時候,線程並不會直接運行,此時會進入就緒狀態(進入可運行線程池),也就表示我準備好了,隨時能夠工做
  • 那麼何時工做呢?這個就不禁咱們來控制了,實際是由線程調度程序從可運行線程池中挑一個線程來工做
  1. 運氣來了,thread線程執行了,假設其從一個阻塞隊列queue中取數據
  • 然而此時queue爲空,致使獲取不到數據,線程被阻塞,等待隊列非空,這個時候線程就由運行狀態進入阻塞狀態了
  • 主線程此時往隊列中塞入一個數據,thread線程被喚醒,此時依然是進入就緒狀態,等待線程調度程序來執行它
  1. 等線程執行完畢後,就進入了死亡狀態,而後就開始gc回收資源了

II. Thread解析

在java這門編程語言中,要使用線程,多半是離不開接觸Thread這個類,爲何會說是多半呢?

由於有些時候,咱們藉助線程池,fork/join等來實現併發時,可能並不須要顯示的利用的Thread類,但底層實際上是離不開的

這裏也不講Thread是怎麼工做的,實現原理啥的,比較複雜,我也莫不許,就從使用角度出發,來看看裏面經常使用的方法,都是幹嗎用的,以及何時用

1. start 方法

第一個就是這個start()方法了,啓動線程

執行該方法以後,線程進入就緒狀態,對使用者而言,但願線程執行就是調用的這個方法(注意調用以後不會當即執行)

這個方法的主要目的就是告訴系統,咱們的線程準備好了,cpu有空了趕忙來執行咱們的線程


2. run 方法

這個就有意思了,咱們採用繼承Thread類來建立線程時,須要覆蓋的就是這個方法,把線程執行的業務邏輯,放在這個方法裏面,可是線程的執行,倒是start()方法

run 方法中爲具體的線程執行的代碼邏輯,通常而言,都不該該被直接進行調用

那麼問題來了,若是直接調用了會怎樣?

直接調用Thread的run方法,並不會報錯,且能夠正常執行,可是執行是在調用這個方法的線程中執行的,不會讓thread這個線程進入就緒狀態,運行狀態啥的,其實質就是一個普通對象的普通方法調用


3. sleep 方法

睡眠一段時間,這個過程當中不會釋放線程持有的鎖, 傳入int類型的參數,表示睡眠多少ms

讓出CUP的使用、目的是不讓當前線程獨自霸佔該進程所獲的CPU資源,以留必定時間給其餘線程執行的機會

咱們最多見的一種使用方式是在主線程中直接調用 Thread.sleep(100) , 表示先等個100ms, 而後再繼續執行


4. wait 方法

wait()方法是Object類裏的方法;當一個線程執行到wait()方法時,它就進入到一個和該對象相關的等待池中,同時失去(釋放)了對象的機鎖(暫時失去機鎖,wait(long timeout)超時時間到後還須要返還對象鎖);其餘線程能夠訪問

wait()使用notify或者notifyAlll或者指定睡眠時間來喚醒當前等待池中的線程

一般咱們執行wait方法是由於當前線程的執行,可能依賴到其餘線程,如登陸線程中,若發現用戶沒有註冊,則等待,等用戶註冊成功後繼續走登陸流程(咱們不考慮這個邏輯是否符合實際),

這裏就能夠在登陸線程中調用 wait方法, 在註冊線程中,在執行完畢以後,調用notify方法通知登陸線程,註冊完畢,而後繼續進行登陸後續action


5. yield 方法

暫停當前正在執行的線程對象,並執行其餘線程

yield()應該作的是讓當前運行線程回到可運行狀態,以容許具備相同優先級的其餘線程得到運行機會。所以,使用yield()的目的是讓相同優先級的線程之間能適當的輪轉執行。可是,實際中沒法保證yield()達到讓步目的,由於讓步的線程還有可能被線程調度程序再次選中

這個方法的執行,有點像一個拿到麪包的人對另外幾我的說,我把麪包放在桌上,咱們重新開始搶,那麼下一個拿到麪包的仍是這些人中的某個(你們機會均等)

想象不出啥時候會這麼幹


6. join 方法

啓動線程後直接調用,即join()的做用是:「等待該線程終止」,這裏須要理解的就是該線程是指的主線程等待子線程的終止。也就是在子線程調用了join()方法後面的代碼,只有等到子線程結束了才能執行

從上面的描述也能夠很容易看出什麼場景須要調用這個方法,主線程和子線程誰先結束很差說,若是主線程提早結束了,致使整個應用都關了,這個時候子線程沒執行完,就呵呵了;

其次就是子線程執行一系列計算,主線程會用到計算結果,那麼就能夠執行這個方法,保證子線程執行完畢後再使用計算結果


7. setDaemon 方法

這個比較有意思,將線程定義爲守護線程,那麼什麼是守護線程?

用個比較通俗的好比,任何一個守護線程都是整個JVM中全部非守護線程的保姆:

只要當前JVM實例中尚存在任何一個非守護線程沒有結束,守護線程就所有工做;只有當最後一個非守護線程結束時,守護線程隨着JVM一同結束工做。

這裏有幾點須要注意:

  1. thread.setDaemon(true)必須在thread.start()以前設置,不然會拋出一個IllegalThreadStateException異常。你不能把正在運行的常規線程設置爲守護線程
  2. 在Daemon線程中產生的新線程也是Daemon的。
  3. 不要認爲全部的應用均可以分配給Daemon來進行服務,好比讀寫操做或者計算邏輯

由於你不可能知道在全部的User完成以前,Daemon是否已經完成了預期的服務任務。一旦User退出了,可能大量數據尚未來得及讀入或寫出,計算任務也可能屢次運行結果不同。這對程序是毀滅性的。形成這個結果理由已經說過了:一旦全部User Thread離開了,虛擬機也就退出運行了

III. 小結

1. 線程狀態

線程有五個狀態

  • new一個線程對象後,首先進入建立狀態
  • 執行Thread#start方法以後,進入就緒狀態
  • 線程調度程序將就緒狀態的線程標記爲運行狀態並真正運行
  • 線程運行過程當中,能夠掛起,進入阻塞狀態,阻塞狀態恢復後,接着進入就緒而不是立馬又恢復運行狀態
  • 線程執行完了,就進入結束/死亡狀態

2. Thread使用注意

  • 線程執行的業務邏輯,放在run()方法中
  • 使用 thread.start() 啓動線程
  • wait方法須要和notify方法配套使用
  • 守護線程必須在線程啓動以前設置
  • 若是須要等待線程執行完畢,能夠調用 join()方法

掃描關注,java分享

QrCode

相關文章
相關標籤/搜索