1. 程式人生 > >Java併發程式設計(一)執行緒定義、狀態和屬性

Java併發程式設計(一)執行緒定義、狀態和屬性

一 、執行緒和程序

1. 什麼是執行緒和程序的區別:
執行緒是指程式在執行過程中,能夠執行程式程式碼的一個執行單元。在java語言中,執行緒有四種狀態:執行 、就緒、掛起和結束。
程序是指一段正在執行的程式。而執行緒有時也被成為輕量級的程序,他是程式執行的最小單元,一個程序可以擁有多個執行緒,各個執行緒之間共享程式的內功空間(程式碼段、資料段和堆空間)及一些程序級的資源(例如開啟的檔案),但是各個執行緒都擁有自己的棧空間。
2. 為何要使用多程序
在作業系統級別上來看主要有以下幾個方面:
- 使用多執行緒可以減少程式的響應時間,如果某個操作和耗時,或者陷入長時間的等待,此時程式講不會響應滑鼠和鍵盤等的操作,使用多執行緒後可以把這個耗時的執行緒分配到一個單獨的執行緒去執行,從而使程式具備了更好的互動性。
- 與程序相比,執行緒建立和切換開銷更小,同時多執行緒在資料共享方面效率非常高。
- 多CPU或者多核計算機本身就具備執行多執行緒的能力,如果使用單個程序,將無法重複利用計算機資源,造成資源的巨大浪費。在多CPU計算機使用多執行緒能提高CPU的利用率。
- 使用多執行緒能簡化程式的結構,使程式便於理解和維護

二、建立執行緒
多執行緒的實現一般有以下三種方法其中前兩種為最常用的方法:
1. 繼承Thread類,重寫run()方法
Thread本質上也是實現了Runnable介面的一個例項。需要注意的是呼叫start()方法後並不是是立即的執行多執行緒的程式碼,而是使該執行緒變為可執行態,什麼時候執行多執行緒程式碼是由作業系統決定的。
以下是主要步驟:
(1)定義Thread類的子類,並重寫該類的run方法,該run方法的方法體就代表了執行緒要完成的任務。因此把run()方法稱為執行體。
(2)建立Thread子類的例項,即建立了執行緒物件。
(3)呼叫執行緒物件的start()方法來啟動該執行緒。

public class TestThread extends Thread{ 
    public void run() {
            System.out.println("Hello World");
        }  
    public static void main(String[] args) {
        Thread mThread = new TestThread();
        mThread.start(); 
    } 
}

2. 實現Runnable介面,並實現該介面的run()方法
以下是主要步驟:
(1)自定義類並實現Runnable介面,實現run()方法。
(2)建立Thread子類的例項,用實現Runnable介面的物件作為引數例項化該Thread物件。
(3)呼叫Thread的start()方法來啟動該執行緒。

public class TestRunnable implements Runnable {
    public void run() { 
            System.out.println("Hello World");
        } 
}

public class TestRunnable {
    public static void main(String[] args) {
        TestRunnable mTestRunnable = new TestRunnable();      
        Thread mThread = new Thread(mTestRunnable);
        mThread.start(); 
    } 
}

3. 實現Callable介面,重寫call()方法
Callable介面實際是屬於Executor框架中的功能類,Callable介面與Runnable介面的功能類似,但提供了比Runnable更強大的功能,主要表現為以下的3點:
(1)Callable可以在任務接受後提供一個返回值,Runnable無法提供這個功能。
(2)Callable中的call()方法可以丟擲異常,而Runnable的run()方法不能丟擲異常。
(3)執行Callable可以拿到一個Future物件,Future物件表示非同步計算的結果,他提供了檢查計算是否完成的方法。由於執行緒屬於非同步計算模型,因此無法從別的執行緒中得到函式的返回值,在這種情況下就可以使用Future來監視目標執行緒呼叫call()方法的情況,但呼叫Future的get()方法以獲取結果時,當前執行緒就會阻塞,直到call()方法的返回結果。

public class TestCallable {  
    //建立執行緒類
    public static class MyTestCallable  implements Callable {  
        public String call() throws Exception {  
             retun "Hello World";
            }  
        }  
public static void main(String[] args) {  
        MyTestCallable mMyTestCallable= new MyTestCallable();  
        ExecutorService mExecutorService = Executors.newSingleThreadPool();  
        Future mfuture = mExecutorService.submit(mMyTestCallable);  
        try { 
        //等待執行緒結束,並返回結果
            System.out.println(mfuture.get());  
        } catch (Exception e) {  
           e.printStackTrace();
        } 
    }  
} 

上述程式的輸出結果為:Hello World

在這三種方式中,一般推薦實現Runnable介面的方式,其原因是:首先,Thread類定義了多種方法可以被派生類使用重寫,但是隻有run()方法是必須被重寫的,實現這個執行緒的主要功能,這也是實現Runnable介面需要的方法。其次,一個類應該在他們需要加強或者修改時才會被繼承。因此如果沒有必要重寫Thread類的其他方法,那麼在這種情況下最好是用實現Runnable介面的方式。

三、中斷執行緒
當執行緒的run()方法執行方法體中的最後一條語句後,並經由執行return語句返回時,或者出現在方法中沒有捕獲的異常時執行緒將終止。在java早期版本中有一個stop方法,其他執行緒可以呼叫它終止執行緒,但是這個方法現在已經被棄用了。
interrupt方法可以用來請求終止執行緒,當一個執行緒呼叫interrupt方法時,執行緒的中斷狀態將被置位。這是每個執行緒都具有的boolean標誌,每個執行緒都應該不時的檢查這個標誌,來判斷執行緒是否被中斷。
要想弄清執行緒是否被置位,可以呼叫Thread.currentThread().isInterrupted():

while(!Thread.currentThread().isInterrupted()){
do something
}

但是如果一個執行緒被阻塞,就無法檢測中斷狀態。這是產生InterruptedException的地方。當一個被阻塞的執行緒(呼叫sleep或者wait)上呼叫interrupt方法。阻塞呼叫將會被InterruptedException中斷。
如果每次迭代之後都呼叫sleep方法(或者其他可中斷的方法),isInterrupted檢測就沒必要也沒用處了,如果在中斷狀態被置位時呼叫sleep方法,它不會休眠反而會清除這一狀態並丟擲InterruptedException。所以如果在迴圈中呼叫sleep,不要去檢測中斷狀態,只需捕獲InterruptedException。
在很多釋出的程式碼中會發現InterruptedException被抑制在很低的層次上:

void myTask(){
...
try{
sleep(50)
}catch(InterruptedException e){
...
}
}

不要這樣做,如果不認為catch中做一處理有什麼好處的話,有兩種合理的選擇:

  • 在catch中呼叫Thread.currentThread().interrup()來設定中斷狀態。呼叫者可以對其進行檢測
  • 更好的選擇用throw InterruptedException標記你的方法,不採用try語句塊來捕獲已成。這樣呼叫者可以捕獲這個異常:
void myTask()throw InterruptedException{
sleep(50)
}

四、執行緒的狀態

(1). 新建狀態(New):新建立了一個執行緒物件。
(2). 就緒狀態(Runnable):執行緒物件建立後,其他執行緒呼叫了該物件的start()方法。該狀態的執行緒位於可執行執行緒池中,變得可執行,等待獲取CPU的使用權。
(3). 執行狀態(Running):就緒狀態的執行緒獲取了CPU,執行程式程式碼。
(4). 阻塞狀態(Blocked):阻塞狀態是執行緒因為某種原因放棄CPU使用權,暫時停止執行。直到執行緒進入就緒狀態,才有機會轉到執行狀態。阻塞的情況分三種:
- 等待阻塞:執行的執行緒執行wait()方法,JVM會把該執行緒放入等待池中。
- 同步阻塞:執行的執行緒在獲取物件的同步鎖時,若該同步鎖被別的執行緒佔用,則JVM會把該執行緒放入鎖池中。
- 其他阻塞:執行的執行緒執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該執行緒置為阻塞狀態。當sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新轉入就緒狀態。
(5). 死亡狀態(Dead):執行緒執行完了或者因異常退出了run()方法,該執行緒結束生命週期。

這裡寫圖片描述

五、執行緒的優先順序和守護執行緒

1. 執行緒優先順序
在java中,每一個執行緒有一個優先順序,預設情況下,一個執行緒繼承它父類的優先順序。可以用setPriority方法提高或降低任何一個執行緒優先順序。可以將優先順序設定在MIN_PRIORITY(在Thread類定義為1)與MAX_PRIORITY(在Thread類定義為10)之間的任何值。執行緒的預設優先順序為NORM_PRIORITY(在Thread類定義為5)。
儘量不要依賴優先順序,如果確實要用,應該避免初學者常犯的一個錯誤。如果有幾個高優先順序的執行緒沒有進入非活動狀態,低優先順序執行緒可能永遠也不能執行。每當排程器決定執行一個新執行緒時,首先會在具有搞優先順序的執行緒中進行選擇,儘管這樣會使低優先順序的執行緒完全餓死。

2. 守護執行緒

呼叫setDaemon(true);將執行緒轉換為守護執行緒。守護執行緒唯一的用途就是為其他執行緒提供服務。計時執行緒就是一個例子,他定時傳送訊號給其他執行緒或者清空過時的告訴快取項的執行緒。當只剩下守護執行緒時,虛擬機器就退出了,由於如果只剩下守護執行緒,就沒必要繼續執行程式了。
另外JVM的垃圾回收、記憶體管理等執行緒都是守護執行緒。還有就是在做資料庫應用時候,使用的資料庫連線池,連線池本身也包含著很多後臺執行緒,監控連線個數、超時時間、狀態等等。