Java核心技術Ι(1)——併發之線程

基本概念

    大家應該都很熟悉操作系統中的多任務(multitasking):在同一時刻運行多個線程的能力。今天人們大多擁有單臺擁有多個CPU的計算機,但是,併發執行的線程數目並不是由CPU數目制約的。操作系統將CPU的時間片分配給每一個進程,給人並行處理的感覺。
    多線程程序在較低的層次上擴展了多任務的概念:一個程序同時執行多個任務。通常每一個任務稱爲一個線程(thread),它是線程控制的簡稱。可以同時運行一個以上線程的程序稱爲多線程程序(multithreaded)。

多線程與多進程的區別:

  1. 每個進程擁有自己的一整套變量,而線程則共享數據,共享數據風險較高,也使得線程之間的通信比進程之間的通信更有效、更容易;
  2. 在有些操作系統中,與進程相比較,線程更“輕量級”,創建、撤銷一個線程比啓動新進程的開下要小得多。

要想在一個單獨的線程中執行一個任務,可以使用Runnable接口,接口定義非常簡單:

public interface Runnable {
    void run();
}

由於Runnable是一個函數式接口,可以用lambda表達式建立一個實例:

Runnable r = () -> { task code };

Runnable創建一個Thread對象:

Thread t = new Thread(r);

啓動線程:

t.start();

也可以通過構建一個 Thread 類的子類定義一個線程, 如下所示:

public myThread extends Thread {
    public void run() {
        task code
    }
}

然後構造一個子類的對象,並調用 start 方法。 不過這種方法已不再推薦,應該將要並行運行的任務與運行機制解耦合。如果有很多任務,要爲每個任務創建一個獨立的線程所付出的代價太大了,可以使用線程池來解決這個問題。
注意:不要調用 Thread 類或繼承了 Runnable 對象的 run方法。直接調用 run方法,只會執行同一個線程中的任務,而不會啓動新線程。應該調用Thread.start() 方法。這個方法將創建一個執行 run方法的新線程。

中斷線程

    當線程的 run 方法執行方法體中最後一條語句後,並經由執行 return 語句返回時,或者出現了在方法中沒有捕獲的異常時,線程將終止。 在 Java 的早期版本中, 還有一個 stop 方法, 其他線程可以調用它終止線程,但是這個方法現在已經被棄用了。—TODO 原因。沒有可以強制線程終止的方法,interrupt 方法可以用來請求終止線程,當對一個線程調用 interrupt 方法的時候,線程的中斷狀態將被置位,中斷狀態位是每個線程都具有的boolean標誌,每個線程都應該不時的檢查這個標誌,以判斷線程是否被中斷。要想弄清中斷狀態是否被置位,首先調用靜態的Thread.currentThread方法獲得當前線程,然後調用isInterrupted方法:

while(!Thread.currentThread().isInterrupted() && more work to do)
{
    do more work
}

如果線程被阻塞就無法檢測中斷狀態。這是產生 InterruptedException 異常的地方,當在一個被阻塞的線程(調用 sleepwait) 上調用 interrupt 方法時,阻塞調用將會被 InterruptedException 異常中斷。沒有任何語言方面的需求要求一個被中斷的線程應該終止,中斷一個線程不過是引起它的注意,被中斷的線程可以決定如何響應中斷。某些非常重要線程可以處理完異常繼續執行,不進行中斷。但是更普遍的情況是,線程簡單地將中斷作爲一個終止的請求,這種線程的run方法具有如下形式:

Runnable r = () -> {
    try {
        ...
        while(!Thread.currentThread().isInterrupted() && more work to do)
        {
            do more work
        }
    } catch (InterruptedException e) {
        // thread was interrupted during sleep or wait
    } finally {
        cleanup, if required
    }
    // exiting the run method terminates the thread
}

如果在每次工作迭代之後都調用 sleep 方法(或者其他的可中斷方法),islnterrupted 檢測既沒有必要也沒有用處。如果在中斷狀態被置位時調用 sleep 方法,它不會休眠。相反,它將清除這一狀態並拋出 InterruptedException 。因此,如果你的循環調用 sleep ,不會檢測中斷狀態。相反,要如下所示捕獲 InterruptedException 異常:

Runnable r = () -> {
    try {
        ...
        while(more work to do)
        {
            do more work
            Thread.sleep(delay);
        }
    } catch (InterruptedException e) {
        // thread was interrupted during sleep
    } finally {
        cleanup, if required
    }
    // exiting the run method terminates the thread
}

interrupt、interrupted 、islnterrupted:

  1. interrupt方法向線程發送中斷請求。線程的中斷狀態將被設置爲 true,如果目前該線程被一個 sleep 調用阻塞,那麼,InterruptedException 異常被拋出。
  2. interrupted 方法是一個靜態方法,它檢測當前的線程是否被中斷。而且,調用 interrupted 方法會清除該線程的中斷狀態;
  3. islnterrupted 方法是一個實例方法,可用來檢驗是否有線程被中斷。調用這個方法不會改變中斷狀態。

在很多代碼中 InterruptedException 直接被忽視了,而沒有處理,更好的方式是:

void mySubTask {
    ...
    try { sleep(delay); }
    catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    ...
}

或者,更好的選擇是,用 throws InterruptedException 標記你的方法, 不採用 try 語句塊捕獲異常。於是調用者(或者, 最終的 run 方法)可以捕獲這一異常。

void mySubTask throws InterruptedException {
    ...
    sleep(delay);
    ...
}

線程狀態

線程可以有如下 6 種狀態:

  • New (新創建)
  • Runnable (可運行)
  • Blocked (被阻塞)
  • Waiting (等待)
  • Timed waiting (計時等待)
  • Terminated (被終止)

線程狀態轉移如圖所示:
在這裏插入圖片描述
上圖展示了線程可以具有的狀態以及從一個狀態到另一個狀態可能的轉換。當一個線程被阻塞或等待時(或終止時)另一個線程被調度爲運行狀態。當一個線程被重新激活(例
如, 因爲超時期滿或成功地獲得了一個鎖,) 調度器檢查它是否具有比當前運行線程更高的優先級,如果是這樣,調度器從當前運行線程中挑選一個, 剝奪其運行權,選擇一個新的線程運行。

新創建線程

    當用 new 操作符創建一個新線程時,如 new Thread(r), 該線程還沒有開始運行。這意味着它的狀態是 new。當一個線程處於新創建狀態時,程序還沒有開始運行線程中的代碼。在線程運行之前還有一些基礎工作要做。

可運行線程

    一旦調用 start 方法,線程處於 runnable 狀態。一個可運行的線程可能正在運行也可能沒有運行, 這取決於操作系統給線程提供運行的時間。(Java 的規範說明沒有將它作爲一個單獨狀態,一個正在運行中的線程仍然處於可運行狀態。)
    一旦一個線程開始運行,它不必始終保持運行。事實上,運行中的線程被中斷,目的是爲了讓其他線程獲得運行機會。線程調度的細節依賴於操作系統提供的服務。搶佔式調度系統給每一個可運行線程一個時間片來執行任務。當時間片用完,操作系統剝奪該線程的運行權, 並給另一個線程運行機會。當選擇下一個線程時, 操作系統考慮線程的優先級。
注意:在任何給定時刻,二個可運行的線程可能正在運行也可能沒有運行

被阻塞線程和等待線程

    當線程處於被阻塞或等待狀態時,它暫時不活動,它不運行任何代碼且消耗最少的資源。直到線程調度器重新激活它,細節取決於它是怎樣達到非活動狀態的。

  • 當一個線程試圖獲取一個內部的對象鎖(而不是 java.util.concurrent 庫中的鎖)而該鎖被其他線程持有,則該線程進人阻塞狀態,當所有其他線程釋放該鎖,並且線程調度器允許本線程持有它的時候,該線程將變成非阻塞狀態。
  • 當線程等待另一個線程通知調度器一個條件時,它自己進入等待狀態。在調用Object.wait 方法或 Thread.join 方法, 或者是等待 java.util.concurrent 庫中的 LockCondition 時, 就會出現這種情況。實際上,被阻塞狀態與等待狀態是有很大不同的。
  • 有幾個方法有一個超時參數,調用它們導致線程進入計時等待(timed waiting ) 狀
    態。這一狀態將一直保持到超時期滿或者接收到適當的通知。帶有超時參數的方法有
    Thread.sleepObject.waitThread.joinLock.tryLock 以及 Condition.await 的計時版。

被終止的線程

    線程因如下兩個原因之一而被終止:

  • 因爲 run 方法正常退出而自然死亡。
  • 因爲一個沒有捕獲的異常終止了 run 方法而意外死亡。
    特別是, 可以調用線程的 stop 方法殺死一個線程。 該方法拋出 ThreadDeath 錯誤對象,
    由此殺死線程。但是 stop 方法已過時, 不要在自己的代碼中調用這個方法
  1. void join()
    等待終止指定的線程。
  2. void join(long millis)
    等待指定的線程死亡或者經過指定的毫秒數。
  3. Thread.state.getState()
    得到這一線程的狀態:NEW、RUNNABLE、BLOCKED、 WAITING、TIMED_WAITING或 TERMINATED 之一。

線程屬性

線程優先級

    在 Java 程序設計語言中,每一個線程有一個優先級。默認情況下, 一個線程繼承它的父線程的優先級。可以用 setPriority 方法提高或降低任何一個線程的優先級。可以將優先級設置爲在 MIN_PRIORITY (在 Thread 類中定義爲 1 ) 與 MAX_PRIORITY (定義爲 10 ) 之間的任何值。NORM_PRIORITY 被定義爲 5。每當線程調度器有機會選擇新線程時, 它首先選擇具有較高優先級的線程。但是線程優先級是高度依賴於系統的。當虛擬機依賴於宿主機平臺的線程實現機制時,Java 線程的優先級被映射到宿主機平臺的優先級上,優先級個數也許更多,也許更少。
注意: 如果確實要使用優先級, 應該避免初學者常犯的一個錯誤。如果有幾個高優先級的線程沒有進入非活動狀態, 低優先級的線程可能永遠也不能執行。每當調度器決定運行一個新線程時,首先會在具有高優先級的線程中進行選擇, 儘管這樣會使低優先級的線程完全餓死。

  1. void setPriority(int newPriority)
    設置線程的優先級,優先級必須在Thread.MIN_PRIORITY 與 Thread.MAX_PRIORITY之間。一般使用Thread.NORM_RIORITY 優先級。
  2. static void yield()
    導致當前執行線程處於讓步狀態。如果有其他的可運行線程具有至少與此線程同樣高的優先級,那麼這些線程接下來會被調度。注意這是一個靜態方法。

守護線程

    可以通過調用 t.setDaemon(true);將線程轉換爲守護線程(daemon thread) 這樣一個線程沒有什麼神奇。守護線程的唯一用途是爲其他線程提供服務。計時線程就是一個例子,它定時地發送“計時器嘀嗒” 信號給其他線程或清空過時的高速緩存項的線程。當只剩下守護線程時, 虛擬機就退出了,由於如果只剩下守護線程, 就沒必要繼續運行程序了。
    守護線程有時會被初學者錯誤地使用, 他們不打算考慮關機(shutdown) 動作,但是這是很危險的。守護線程應該永遠不去訪問固有資源, 如文件、 數據庫,因爲它會在任何時候甚至在一個操作的中間發生中斷。

void setDaemon( boolean isDaemon )
標識該線程爲守護線程或用戶線程。這一方法必須在線程啓動之前調用。

未捕獲異常處理器

    線程的 run 方法不能拋出任何受查異常, 但是非受査異常會導致線程終止,在這種情況下,線程就死亡了。但是,不需要任何 catch 子句來處理可以被傳播的異常。相反,就在線程死亡之前異常被傳遞到一個用於未捕獲異常的處理器。該處理器必須屬於一個實現 Thread.UncaughtExceptionHandler 接口的類,這個接口只有—個方法。

void uncaughtException(Thread t, Throwable e);

可以用 setUncaughtExceptionHandler 方法爲任何線程安裝一個處理器。也可以用 Thread 類的靜態方法 setDefaultUncaughtExceptionHandler 爲所有線程安裝一個默認的處理器。替換處理器可以使用日誌 API 發送未捕獲異常的報告到日誌文件。如果不安裝默認的處理器, 默認的處理器爲空。但是, 如果不爲獨立的線程安裝處理器,此時的處理器就是該線程的 ThreadGroup 對象。線程組是一個可以統一管理的線程集合。默認情況下,創建的所有線程屬於相同的線程組, 但是, 也可能會建立其他的組。現在引入了更好的特性用於線程集合的操作,所以建議不要在自己的程序中使用線程組。ThreadGroup 類實現 Thread.UncaughtExceptionHandler 接口。它的 uncaughtException 方法做如下操作:

  1. 如果該線程組有父線程組, 那麼父線程組的 uncaughtException 方法被調用。
  2. 否則, 如果 Thread.getDefaultExceptionHandler 方法返回一個非空的處理器, 則調用該處理器。
  3. 否則,如果 ThrowableThreadDeath 的一個實例, 什麼都不做。
  4. 否則,線程的名字以及 Throwable 的棧軌跡被輸出到 System.err 上。

這是你在程序中肯定看到過許多次的棧軌跡。

【java.lang.Thread】

  1. static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler)
    static Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler()
    設置或獲取未捕獲異常的默認處理器
  2. void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler)
    Thread.UncaughtExceptionHandler getUncaughtExceptionHandler()
    設置或獲取未捕獲異常的處理器。如果沒有安裝處理器,則將線程組對象作爲處理器。

【java.lang.Thread.UncaughtExceptionHandler】
void uncaughtException(Thread t, Throwable e)
當一個線程因未捕獲的異常而終止,按規定要將客戶報告記錄到日誌中。
參數:
t——由於未捕獲異常而終止的線程
e——未捕獲的異常對象

【java.lang.ThreadGroup】
void uncaughtException(Thread t, Throwable e)
如果有父線程組,調用父線程組的這一方法;或者,如果Thread類有默認處理器,調用該處理器,否則,輸出棧軌跡到標準錯誤流上(但是,如果e是一個ThreadDeath對象,棧軌跡是被禁用的。不過ThreadDeath對象由stop方法產生,而該方法已經過時)。

參考

《Java核心技術 卷1》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章