Java併發編程(一)線程的定義、狀態、屬性

本系列爲學習筆記,學習內容主要來自劉望舒的博客

1. 線程的定義、狀態、屬性

  • 進程是指一段正在執行的程序。而線程有時也被稱爲輕量級的進程,他是程序執行的最小單元。
  • 一個進程可以擁有多個線程,各個線程之間共享程序的內存空間(代碼段、數據段和堆空間)及一些進程級的資源(例如打開的文件),但是各個線程都擁有自己的棧空間

1. 爲何?

從操作系統的角度而言:
- 使用多線程可以減少程序的響應時間,如果某個操作很耗時,或者陷入長時間的等待,此時程序講不會響應鼠標和鍵盤等的操作,使用多線程後可以把這個耗時的線程分配到一個單獨的線程去執行,從而使程序具備了更好的交互性。
- 與進程相比,線程創建和切換開銷更小,同時多線程在數據共享方面效率非常高。
- 多CPU或者多核計算機本身就具備執行多線程的能力,如果使用單個進程,將無法重複利用計算機資源,造成資源的巨大浪費。在多CPU計算機使用多線程能提高CPU的利用率。
- 使用多線程能簡化程序的結構,使程序便於理解和維護

2. 創建線程

1. 繼承Thread類,重寫run()方法

  • Thread本質上是實現了Runnable接口的一個實例
  • 調用start()方法後並不是立即執行多線程的代碼,而是使該線程變爲可運行態,什麼時候運行多線程代碼是由操作系統決定的。
  • 主要步驟:
    • 定義Thread類的子類,並重寫該類的run方法,該run方法的方法體代表了線程要完成的任務,稱爲執行體。
    • 創建Thread子類的實例,即創建了線程對象
    • 調用線程對象的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()方法

主要步驟:
- 自定義類並實現Runnable接口,實現run()方法
- 創建Thread子類的實例,用實現Runnable接口的對象作爲參數實例化該Thread對象
- 調用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更強大的功能
- Callable可以在任務接受後提供一個返回值,Runnable無法提供這個功能。
- Callable中的call()方法可以拋出異常,而Runnable的run()方法不能拋出異常。
- 運行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();
        } 
    }  
}

2. 中斷線程

  • 當線程的run()方法執行方法體中的最後一條語句後,並經由執行return語句返回時,或者出現在方法中沒有捕獲的異常時線程將終止。
  • interrupt方法可以用來請求終止線程,當一個線程調用interrupt方法時,線程的中斷狀態將被置位。這是每個線程都具有的boolean標誌,每個線程都應該不時的檢查這個標誌,來判斷線程是否被中斷。

要想弄清線程是否被置位,可以調用Thread.currentThread().isInterrupted():

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

但是如果一個線程被阻塞,就無法檢測中斷狀態。這是產生InterruptedException的地方。當一個被阻塞的線程(調用sleep或者wait)上調用interrupt方法。阻塞調用將會被InterruptedException中斷。

如果每次迭代之後都調用sleep方法(或者其他可中斷的方法),isInterrupted檢測就沒必要也沒用處了,如果在中斷狀態被置位時調用sleep方法,它不會休眠反而會清除這一狀態並拋出InterruptedException。所以如果在循環中調用sleep,不要去檢測中斷狀態,只需捕獲InterruptedException

有兩種合理的選擇:
- 在catch中調用Thread.currentThread().interrup()來設置中斷狀態。調用者可以對其進行檢測
- 更好的選擇用throw InterruptedException標記你的方法,不採用try語句塊來捕獲已成。這樣調用者可以捕獲這個異常:

void myTask()throw InterruptedException{
sleep(50)
}

3. 線程的狀態

  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()方法,該線程結束生命週期。
    線程狀態

4. 線程的優先級和守護線程

線程優先級

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

守護線程
  • 調用setDaemon(true);將線程轉換爲守護線程。
  • 守護線程唯一的用途就是爲其他線程提供服務。計時線程就是一個例子,他定時發送信號給其他線程或者清空過時的告訴緩存項的線程。當只剩下守護線程時,虛擬機就退出了,由於如果只剩下守護線程,就沒必要繼續運行程序了。
  • 另外JVM的垃圾回收、內存管理等線程都是守護線程。還有就是在做數據庫應用時候,使用的數據庫連接池,連接池本身也包含着很多後臺線程,監控連接個數、超時時間、狀態等等。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章