併發程式設計——執行緒的定義、六種狀態的轉換和屬性
注:本篇部落格有很多地方會用上本帥博主以前寫過的部落格,屆時或貼原文連結,或摘抄部分內容。各位看官可以根據自己的需求選擇。
併發程式設計,自然就會涉及到多執行緒;而多執行緒,那自然是由執行緒組成的咯。所以俺們今兒個就來系統地瞭解一下執行緒,瞅瞅它的定義、以及那幾種狀態和屬性。
1.執行緒
執行緒是指程式在執行過程中,能夠執行程式程式碼的一個執行單元。 |
說到執行緒那麼必然會提及程序,以及二者之間的區別。詳情請參考:程序與執行緒的區別以及如何選擇
2. 為何要使用多執行緒
在作業系統級別上來看主要有以下幾個方面:
- 使用多執行緒可以減少程式的響應時間,如果某個操作和耗時,或者陷入長時間的等待,此時程式講不會響應滑鼠和鍵盤等的操作,使用多執行緒後可以把這個耗時的執行緒分配到一個單獨的執行緒去執行,從而使程式具備了更好的互動性。
- 與程序相比,執行緒建立和切換開銷更小,同時多執行緒在資料共享方面效率非常高。
- 多CPU或者多核計算機本身就具備執行多執行緒的能力,如果使用單個程序,將無法重複利用計算機資源,造成資源的巨大浪費。在多CPU計算機使用多執行緒能提高CPU的利用率。
- 使用多執行緒能簡化程式的結構,使程式便於理解和維護
3.建立執行緒
建立執行緒有三種方式,分別為
- 繼承Thread類,重寫run()方法
- 實現Runnable介面,並實現該介面的run()方法
- 實現Callable介面,重寫call()方法
注:一般情況下推薦使用第二種方法。
關於這三種方式,本帥博主在之前的部落格中有過整理,這裡就不贅述了,詳情請見:實現Java多執行緒的三種方法
4.執行緒的六種狀態
執行緒一共具有六種狀態,簡介版如下圖:
複雜版如下圖:
(1)New 新建狀態:新建的Thread物件,未呼叫start()
(2)Runnable 可執行狀態:可執行狀態中包含就緒態與執行態
- 就緒態:已經獲取所有資源,CPU分配執行權就可以執行,所有就緒態執行緒都在就緒佇列中。即在就緒狀態的程序除CPU之外,其它的執行所需資源都已全部獲得。
-
就緒狀態的執行緒來源如下:
(2.1)就緒狀態只是說你資格執行,排程程式沒有挑選到你,你就永遠是就緒狀態。
(2.2)呼叫執行緒的start()方法,此執行緒進入就緒狀態。
(2.3)當前執行緒sleep()方法結束,其他執行緒join()結束,等待使用者輸入完畢, 進入就緒狀態
(2.4)當前執行緒時間片用完了,呼叫當前執行緒的yield()方法,當前執行緒進入就緒狀態。
(2.5)鎖池裡的執行緒拿到物件鎖後,進入就緒狀態。 - 執行態:正在執行的執行緒,一個CPU同一時間只能處理一個執行緒,因此一個CPU上只有一個執行態程式
(3)Blocked 阻塞狀態:執行緒請求資源失敗時會進入該狀態。所有阻塞態執行緒都儲存在一個阻塞佇列中,阻塞態執行緒會不斷請求資源成功後進入就緒佇列。待執行。
阻塞的狀態分為三種:
- 等待阻塞:執行的執行緒執行wait()方法,該執行緒會釋放佔用的所有資源,JVM會把該執行緒放入“等待池”中。進入這個狀態後,是不能自動喚醒的,必須依靠其他執行緒呼叫notify()或notifyAll()方法才能被喚醒,
- 同步阻塞:執行的執行緒在獲取物件的同步鎖時,若該同步鎖被別的執行緒佔用,則JVM會把該執行緒放入“鎖池”中。
- 其他阻塞:執行的執行緒執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該執行緒置為阻塞狀態。當sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新轉入就緒狀態。
(4)Waiting 等待狀態(即無限期等待):wait、join等函式會造成進入該狀態。同樣有一個等待佇列儲存所有等待執行緒,執行緒會等待其他執行緒指示才能繼續執行。等待狀態執行緒主動放棄CPU執行權。
以下方法會讓執行緒陷入無限期等待狀態:
- 沒有設定timeout引數的Object.wait()
- 沒有設定timeout引數的Thread.join()
- LockSupport.park()
注意:LockSupport.park(Object blocker) 會掛起當前執行緒,引數blocker是用於設定當前執行緒的“volatile Object parkBlocker 成員變數”
parkBlocker 是用於記錄執行緒是被誰阻塞的,可以通過LockSupport.getBlocker()獲取到阻塞的物件,用於監控和分析執行緒用的。
“阻塞”與“等待”的區別:
- “阻塞”狀態是等待著獲取到一個排他鎖,進入“阻塞”狀態都是被動的,離開“阻塞”狀態是因為其它執行緒釋放了鎖,不阻塞了;
- “等待”狀態是在等待一段時間 或者 喚醒動作的發生,進入“等待”狀態是主動的。如主動呼叫Object.wait(),如無法獲取到ReentraantLock,主動呼叫LockSupport.park(),如主執行緒主動呼叫 subThread.join(),讓主執行緒等待子執行緒執行完畢再執行。離開“等待”狀態是因為其它執行緒發生了喚醒動作或者到達了等待時間
(5)Timed_Waiting 計時等待:計時等待也是主動放棄CPU執行權的,區別是,超時後會進入阻塞態競爭資源
以下方法會讓執行緒進入TIMED_WAITING限期等待狀態:
- Thread.sleep()方法
- 設定了timeout引數的Object.wait()方法
- 設定了timeout引數的Thread.join()方法
- LockSupport.parkNanos()方法
- LockSupport.parkUntil()方法
(6)Terminal 結束狀態:執行緒執行結束後的狀態。
注:上面第二章圖中所言“獲取到鎖”,即
拿到物件的鎖標記,即為獲得了對該物件(臨界區)的使用許可權。即該執行緒獲得了執行所需的資源,進入“就緒狀態”,只需獲得CPU,就可以執行。因為當呼叫wait()後,執行緒會釋放掉它所佔有的“鎖標誌”,所以執行緒只有在此獲取資源才能進入就緒狀態。
下面對上述流程圖的轉換作出一些解釋:
(1)執行緒的實現有三種方式,當我們new了這個物件後,執行緒就進入了初始狀態;
(2)當該物件呼叫了start()方法,就進入就緒狀態;
(3)進入就緒後,當該物件被作業系統選中,獲得CPU時間片就會進入執行狀態;
(4)進入執行狀態後情況就比較複雜了
(4.1)run()方法或main()方法結束後,執行緒就進入終止狀態;
(4.2)當執行緒呼叫了自身的sleep()方法或其他執行緒的join()方法,程序讓出CPU,然後就會進入阻塞狀態(該狀態既停止當前執行緒,但並不釋放所佔有的資源即呼叫sleep ()函式後,執行緒不會釋放它的“鎖標誌”。)。當sleep()結束或join()結束後,該執行緒進入可執行狀態,繼續等待OS分配CPU時間片。典型地,sleep()被用在等待某個資源就緒的情形:測試發現條件不滿足後,讓執行緒阻塞一段時間後重新測試,直到條件滿足為止。
(4.3)執行緒呼叫了yield()方法,意思是放棄當前獲得的CPU時間片,回到就緒狀態,與其他執行緒一起競爭,OS有可能會接著又讓這個程序進入執行狀態(至於什麼時候,那就得看它啥時候競爭到); 呼叫 yield() 的效果等價於排程程式認為該執行緒已執行了足夠的時間片從而需要轉到另一個執行緒。yield()只是使當前執行緒重新回到可執行狀態,所以執行yield()的執行緒有可能在進入到可執行狀態後馬上又被執行。
(4.4)當執行緒剛進入可執行狀態(注意,還沒執行),發現將要呼叫的資源被synchronized(同步),獲取不到鎖標記,將會立即進入鎖池狀態,等待獲取鎖標記(這時的鎖池裡也許已經有了其他執行緒在等待獲取鎖標記,這時它們處於佇列狀態,既先到先得),一旦執行緒獲得鎖標記後,就轉入就緒狀態,等待OS分配CPU時間片;
(4.5)suspend() 和 resume()方法:兩個方法配套使用,suspend()使得執行緒進入阻塞狀態,並且不會自動恢復,必須其對應的resume()被呼叫,才能使得執行緒重新進入可執行狀態。典型地,suspend()和 resume() 被用在等待另一個執行緒產生的結果的情形:測試發現結果還沒有產生後,讓執行緒阻塞,另一個執行緒產生了結果後,呼叫 resume()使其恢復。
(4.6)wait()和 notify() 方法:當執行緒呼叫wait()方法後會進入等待佇列(進入這個狀態會釋放所佔有的所有資源,與阻塞狀態不同),進入這個狀態後,是不能自動喚醒的,必須依靠其他執行緒呼叫notify()或notifyAll()方法才能被喚醒(由於notify()只是喚醒一個執行緒,但我們由不能確定具體喚醒的是哪一個執行緒,即隨機喚醒。因此我們需要喚醒的執行緒可能不能夠被喚醒,所以在實際使用時,一般都用notifyAll()方法,喚醒有所執行緒),執行緒被喚醒後會進入鎖池,等待獲取鎖標記。
wait() 使得執行緒進入阻塞狀態,它有兩種形式:
一種允許指定以毫秒為單位的一段時間作為引數;另一種沒有引數。前者當對應的 notify()被呼叫或者超出指定時間時執行緒重新進入可執行狀態即就緒狀態,後者則必須對應的 notify()被呼叫。當呼叫wait()後,執行緒會釋放掉它所佔有的“鎖標誌”,從而使執行緒所在物件中的其它synchronized資料可被別的執行緒使用。wait()和notify()因為會對物件的“鎖標誌”進行操作,所以它們必須在synchronized函式或synchronizedblock中進行呼叫。如果在non-synchronized函式或non-synchronizedblock中進行呼叫,雖然能編譯通過,但在執行時會發生IllegalMonitorStateException的異常。
當使用wait()方法之後,執行緒會進入等待佇列,如下圖:
圖中有一個同步佇列,同步佇列是幹啥的呢?
當前執行緒想呼叫物件A的同步方法時,發現物件A的鎖被別的執行緒佔有,此時當前執行緒進入同步佇列。簡言之,同步佇列裡面放的都是想爭奪物件鎖的執行緒。 當一個執行緒1被另外一個執行緒2喚醒時,1執行緒進入同步佇列,去爭奪物件鎖。 同步佇列是在同步的環境下才有的概念,一個物件對應一個同步佇列。 |
————————————————————————————————————————————————————————
以下是wait()和notify()方法和suspend()與resume()方法的比較 |
OK,看完上面冗長的一段,這裡對上述(4.5)和(4.6)中的兩個方法(即wait() 和 notify() 方法與suspend()和 resume() 方法)對做出區別總結:
核心區別:suspend()及其它所有方法線上程阻塞時都不會釋放佔用的鎖(如果佔用了的話),而wait() 和 notify() 這一對方法則相反。
此外,上述這一核心區別還導致了其它一系列的細節上的區別:
- 首先,前面敘述的所有方法都隸屬於 Thread類,但是wait() 和 notify() 方法這一對卻直接隸屬於 Object 類,也就是說,所有物件都擁有這一對方法。初看起來這十分不可思議,但是實際上卻是很自然的,因為這一對方法阻塞時要釋放佔用的鎖,而鎖是任何物件都具有的,呼叫任意物件的 wait() 方法導致執行緒阻塞,並且該物件上的鎖被釋放。而呼叫任意物件的notify()方法則導致因呼叫該物件的 wait()方法而阻塞的執行緒中隨機選擇的一個解除阻塞(但要等到獲得鎖後才真正可執行)。
- 其次,前面敘述的所有方法都可在任何位置呼叫,但是wait() 和 notify() 方法這一對方法卻必須在 synchronized 方法或塊中呼叫,理由也很簡單,只有在synchronized方法或塊中當前執行緒才佔有鎖,才有鎖可以釋放。同樣的道理,呼叫這一對方法的物件上的鎖必須為當前執行緒所擁有,這樣才有鎖可以釋放。因此,這一對方法呼叫必須放置在這樣的 synchronized方法或塊中,該方法或塊的上鎖物件就是呼叫這一對方法的物件。若不滿足這一條件,則程式雖然仍能編譯,但在執行時會出現IllegalMonitorStateException異常。
wait() 和 notify()方法的上述特性決定了它們經常和synchronized方法或塊一起使用,如果將它們和作業系統的程序間通訊機制作一個比較就會發現它們的相似性:
synchronized方法或塊提供了類似於作業系統原語的功能,它們的執行不會受到多執行緒機制的干擾,而這一對方法則相當於 block和wake up 原語(前提是這一對方法均宣告為 synchronized)。它們的結合使得我們可以實現作業系統上一系列精妙的程序間通訊的演算法(如訊號量演算法),並用於解決各種複雜的執行緒間通訊問題。
總而言之,關於 wait() 和 notify() 方法,我們需要格外注意以下兩點:
- 第一:呼叫notify() 方法導致解除阻塞的執行緒是從因呼叫該物件的 wait()方法而阻塞的執行緒中隨機選取的,我們無法預料哪一個執行緒將會被選擇,所以使用時要特別小心,避免因這種不確定性而產生問題。
- 第二:除了notify(),還有一個方法 notifyAll()也可起到類似作用,唯一的區別在於,呼叫 notifyAll()方法將把因呼叫該物件的 wait()方法而阻塞的所有執行緒一次性全部解除阻塞。當然,解除阻塞的執行緒還得競爭,只有獲得鎖的那一個執行緒才能進入可執行狀態。
注:suspend()方法和不指定超時期限的wait()方法的呼叫都可能產生死鎖。使用的時候需要注意。
5.執行緒的優先順序和守護執行緒
1. 執行緒優先順序
在java中,每一個執行緒有一個優先順序,預設情況下,一個執行緒繼承它父類的優先順序。可以用setPriority方法提高或降低任何一個執行緒優先順序。可以將優先順序設定在MIN_PRIORITY(在Thread類定義為1)與MAX_PRIORITY(在Thread類定義為10)之間的任何值。執行緒的預設優先順序為NORM_PRIORITY(在Thread類定義為5)。
不過,建議大家儘量不要依賴優先順序,如果確實要用,應該避免初學者常犯的一個錯誤:餓死,即如果有幾個高優先順序的執行緒沒有進入非活動狀態,低優先順序執行緒可能永遠也不能執行。
2. 守護執行緒
呼叫setDaemon(true);將執行緒轉換為守護執行緒。守護執行緒唯一的用途就是為其他執行緒提供服務。計時執行緒就是一個例子,他定時傳送訊號給其他執行緒或者清空過時的告訴快取項的執行緒。當只剩下守護執行緒時,虛擬機器就退出了,由於如果只剩下守護執行緒,就沒必要繼續執行程式了。
另外JVM的垃圾回收、記憶體管理等執行緒都是守護執行緒。還有就是在做資料庫應用時候,使用的資料庫連線池,連線池本身也包含著很多後臺執行緒,監控連線個數、超時時間、狀態等等。
好啦,以上就是關於執行緒的定義以及執行緒的六種狀態的轉換(重點)與相關方法與屬性的相關知識總結啦,如果大家有什麼不明白的地方或者發現文中有描述不好的地方,歡迎大家留言評論,我們一起學習呀。
Biu~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~pia!