1. 程式人生 > >執行緒的狀態、分類及優先順序

執行緒的狀態、分類及優先順序

       上一篇《執行緒的概念及簡單實現》博文我們簡單地認識了關於多執行緒的概念以及使用Java語言實現多執行緒,這算是我們對Java併發程式設計學習的一個入門吧,那本篇博文我們將繼續更深入地學習多執行緒方面的知識。

執行緒的狀態

       同樣的,執行緒作為一項任務的執行者,從開啟、執行到終結、銷燬,都有它自己的生命週期。那在Java定義中,一般執行緒可以分為六種狀態,我們通過檢視Thread類原始碼發現,在Thread類中定義了一個執行緒狀態的列舉,程式碼如下:
public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,

        /**
         * ...
         */
        RUNNABLE,

        /**
         * ...
         */
        BLOCKED,

        /**
         * ...
         */
        WAITING,

        /**
         *...
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }
下面我們對每一種執行緒狀態進行簡要的說明:        1. 新生(New):在Java中使用new Thread(r)來建立一個新的執行緒,此時執行緒處於新生的狀態,但是程式還沒有開始執行執行緒的程式碼;        2. 可執行(Runnable):在Java中當呼叫start()方法後,執行緒進入了可執行的狀態,此時執行緒可能正在執行,也可能沒有執行,這個取決於CPU是否給當前執行緒提供了時間片;        3. 阻塞(Blocked):一般在如下情況下,執行緒會處於阻塞狀態:當執行緒想要獲取內部的物件鎖,而該鎖被其它執行緒持有,此時執行緒陷入阻塞;        4. 等待(Wait):
當執行緒呼叫Thread.join()方法將CPU的執行權力(即時間片執行權力)出讓給其它執行緒時或呼叫wait()方法讓當前執行緒等待其它執行緒資源執行時,執行緒處於等待狀態。阻塞狀態和等待狀態的區別還是比較大的。
       5. 計時等待(Timed waiting):計時等待和上面所說的等待狀態類似,不同的是計時等待是在其所設定的超時時間後從等待狀態自動自我喚醒,然後進入到可執行狀態,而前者是需要其它執行緒進行手動喚醒。那一般設定定時等待的方法有sleep(long millis)、wait(long timeout)或join(long millis)。        6. 終止(Terminated) :
而執行緒終止或銷燬一般是會在以下兩種情況下發生:1. run()方法中的所有耗時操作都執行完畢,程式流正常退出;2. 在run()方法中有未捕獲的異常而導致程式的非正常退出。
基本瞭解了執行緒的六種狀態後,下面我們通過一張執行緒的狀態圖來進步瞭解一下執行緒六種狀態之間的關係和切換過程,如圖下:

執行緒的分類

       執行緒根據作用可以分為使用者執行緒守護執行緒,一般我們平時用於執行具體業務邏輯的執行緒都是使用者執行緒,而守護執行緒則一般是為正在執行的使用者執行緒提供便利服務的。比方說JVM中的GC(垃圾回收器)執行緒,它就是一個守護執行緒,它的作用就是回收其它普通使用者執行緒執行完成後遺留下的記憶體資源。那我們就會想,若在JVM中的普通使用者執行緒執行完各自的業務邏輯後消亡,守護執行緒依然在執行,那JVM例項還有存在的必要嗎?答案當然是 -- NO。JVM例項不會因為守護執行緒沒有終止而繼續存在,它會在所有普通使用者執行緒終止後退出。       那既然JVM例項並不會因為守護執行緒正在執行而持續保持,那為了避免操作和物件的完整性,我們不應該將一些較為重要的業務邏輯放在守護執行緒中執行,如檔案、資料庫的操作,因為它有可能在任何時候甚至在一個操作的中間發生中斷。守護執行緒並不僅僅存在於系統級別,我們自己也可以建立守護執行緒,我們只需要Thread物件在呼叫start()方法之前呼叫setDaemon(boolean on)方法,將引數設定為true,那我們所建立的執行緒便是守護執行緒。

執行緒的優先順序

       談到執行緒的優先順序,可能許多讀者認為這部分知識並沒有單獨拿出來學習的必要,因為在java中,執行緒的優先順序無非就是為執行緒設定從1-10之間的執行緒等級,在程式執行時,執行緒排程器會首先選擇具有較高優先等級的執行緒。其實上面讀者對執行緒優先順序的理解並沒有錯,但理解得並不是很深刻和全面,因為執行緒的優先順序是高度依賴系統的,不同的作業系統平臺的執行緒實現機制是不同的。就拿Windows 和Linux作業系統來講,Windows系統有7個執行緒優先級別,而Linux系統擁有2的31次方執行緒優先級別。當Java虛擬機器依賴於宿主主機平臺(Linux或Windows)的執行緒實現機制時,java執行緒的優先順序會被對映到宿主主機平臺的優先順序上,對於擁有2的31次方優先等級的Linux系統來說,支援對映java 10個執行緒優先等級當然沒有問題,但對於只有7個執行緒優先等級的Windows系統來說,要對映到Java的10個執行緒優先等級上,勢必會造成很多問題,例如可能Java裡面的優先順序1、2會等同於Windows系統裡的1等級,或是執行緒優先順序8、9、10等同於Windows系統裡的7等級。那此種情況下,Java的執行緒優先順序的設定就沒什麼作用了。        另外Java程式是執行在Java虛擬機器中的,而由於虛擬機器版本的差異化,也會導致執行緒優先順序的對映產生不同的結果,例如Sun為Linux提供的Java虛擬機器,執行緒的優先順序被忽略--即所有執行緒具有相同的優先順序。        前面講了那麼多文縐縐的理論知識,自己都感覺有點磨嘰了,下面我們來學習如何使用Java為執行緒設定優先順序。一般我們可以通過呼叫setPriority(int newPriority)方法為執行緒設定優先順序,方法中傳遞1-10之間的常量值,用於設定執行緒優先等級,1為最低優先順序(在Java中可以用Thread.MIN_PRIORITY),10為最高優先順序(在程式中可以用Thread.MAX_PRIORITY),如何不設定的話,預設執行緒等級為5(在程式中可以用Thread.NORM_PRIORITY)。下面這個實驗,我們建立三個執行緒,模擬三個售票視窗售票,並且為每個執行緒設定不同的優先順序,觀察在設定了優先順序後,每個售票視窗售票情況是否有明顯的差別。我們先來看下程式碼,如下:
package com.androidleaf.multithreading.priority;

public class ThreadPriority {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		MyRunnable myRunnable = new MyRunnable();
		Thread mThread0 = new Thread(myRunnable);
		//設定最高優先順序,10
		mThread0.setPriority(Thread.MAX_PRIORITY);
		Thread mThread1 = new Thread(myRunnable);
		//預設優先順序,5
		mThread1.setPriority(Thread.NORM_PRIORITY);
		Thread mThread2 = new Thread(myRunnable);
		//設定最低優先順序,1
		mThread2.setPriority(Thread.MIN_PRIORITY);
		
		mThread0.start();
		mThread1.start();
		mThread2.start();
		
	}
}

class MyRunnable implements Runnable{

	private int tickets = 50000;
	private int threadFirst = 0;
	private int threadSecond = 0;
	private int threadThird = 0;
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(true){
			synchronized (this) {
				if(tickets > 0){
					System.out.println("總票數:" + tickets + "  視窗號:"
							+ Thread.currentThread().getName()
							+ " 出售1張        剩下票數:" + --tickets);
					if(Thread.currentThread().getName().equals("Thread-0")){
						++ threadFirst;
					}else if(Thread.currentThread().getName().equals("Thread-1")){
						++ threadSecond;
					}else{
						++ threadThird;
					}
				}else{
					break;
				}
			}
		}
		//售票次數只打印一次
		if(Thread.currentThread().getName().equals("Thread-0")){
			System.out.println("第一售票視窗售票數:"+ threadFirst);
			System.out.println("第二售票視窗售票數:"+ threadSecond);
			System.out.println("第三售票視窗售票數:"+ threadThird);
		}
	}
}

程式中,為了讓測試結果更具有可靠性,我們儘量讓售票數較大(這裡取50000張),然後三個視窗開始售票,最後當售票結束後,統計每個視窗的售票數。我們通過數十次的測試後,取得了一組售票結果的資料,我分別取了最具有代表性的的六組資料,分別如下:
第一組售票資料結果:
	第一售票視窗售票數:43147
	第二售票視窗售票數:5429
	第三售票視窗售票數:1424
	
第二組售票資料結果:
	第一售票視窗售票數:13290
	第二售票視窗售票數:35528
	第三售票視窗售票數:1182
	
第三組售票資料結果:
	第一售票視窗售票數:45318
	第二售票視窗售票數:3905
	第三售票視窗售票數:777
	
第四組售票資料結果:
	第一售票視窗售票數:16950
	第二售票視窗售票數:32200
	第三售票視窗售票數:850
	
第五組售票資料結果:
	第一售票視窗售票數:44568
	第二售票視窗售票數:3288
	第三售票視窗售票數:2144
	
第六組售票資料結果:
	第一售票視窗售票數:41628
	第二售票視窗售票數:3928
	第三售票視窗售票數:4444
觀察上面六組售票資料結果,我們可以發現,一般的執行緒優先順序越高的執行緒,它所得到的執行的機會越多,那最終所出售的票數就越多,但這也不是嚴格按照這個規律來進行的。比如我們看一下第二和第四組資料,我們發現,雖然第一售票視窗的執行緒優先順序比第二視窗的執行緒優先順序更高,但第二視窗所出售的票比第一視窗要更多。同樣的,在看下第六組資料,比較第二視窗和第三視窗的售票數情況,也是一樣的現象。        至此,通過上面的實驗我們可知,在我們編寫多執行緒程式時,不能過度使用執行緒的優先順序,特別是不能將構建正確的程式建立在依賴執行緒優先順序的基礎之上,因為程式並不保證擁有較高優先順序的執行緒一定會有更多的執行資源。況且執行緒的優先順序嚴重依賴具體作業系統和虛擬機器版本,在Java跨平臺開發大行其道的今天,若將使用過多優先順序的Java程式部署在不同的系統平臺上,會產生一些意想不到的問題。所以,以後若要使用執行緒的優先順序,要三思啊!! 小結:今天我們學習了一些執行緒的特性,主要包括:(1)執行緒的各種狀態;(2)執行緒的分類;(3)執行緒的優先順序。我們著重講了一下執行緒的各種狀態的特點,並通過一張狀態圖清晰地瞭解了狀態之間的關係和切換條件。還有就是分析了執行緒的優先順序方面的知識,Java中執行緒的優先順序嚴重依賴具體的作業系統和虛擬機器的版本,因此我們使用的時候需謹慎。下一篇博文我們將討論執行緒的中斷和阻塞相關的知識,敬請期待!