Java併發編程指南(一):線程管理

1. 線程的創建和運行:

在Java中,我們有2個方式創建線程:

  • 通過直接繼承Thread類,然後覆蓋run()方法。
  • 構建一個實現Runnable接口的類, 然後創建一個thread類對象並傳遞Runnable對象作爲構造參數

2.獲取和設置線程信息:

Thread類的對象中保存了一些屬性信息能夠幫助我們來辨別每一個線程,知道它的狀態,調整控制其優先級。 這些屬性是:

  • ID: 每個線程的獨特標識。
  • Name: 線程的名稱。
  • Priority: 線程對象的優先級。優先級別在1-10之間,1是最低級,10是最高級。不建議改變它們的優先級,但是你想的話也是可以的。
  • Status: 線程的狀態。在Java中,線程只能有這6種中的一種狀態: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, 或 TERMINATED.

3. 線程的中斷:

一個多個線程在執行的Java程序,只有當其全部的線程執行結束時(更具體的說,是所有非守護線程結束或者某個線程調用System.exit()方法的時候),它纔會結束運行。有時,你需要爲了終止程序而結束一個線程,或者當程序的用戶想要取消某個Thread對象正在做的任務。

Java提供中斷機制來通知線程表明我們想要結束它。中斷機制的特性是線程需要檢查是否被中斷,而且還可以決定是否響應結束的請求。所以,線程可以忽略中斷請求並且繼續運行。

Thread 類還有一個boolean類型的屬性來表明線程是否被中斷。當你調用線程的interrupt() 方法,就代表你把這個屬性設置爲 true。 而isInterrupted() 方法僅返回屬性值。

Thread 類還有其他可以檢查線程是否被中斷的方法。例如,這個靜態方法interrupted()能檢查正在運行的線程是否被中斷。
isInterrupted()和interrupted() 方法有着很重要的區別。第一個不會改變interrupted屬性值,但是第二個會設置成false。

4. 操作線程的中斷機制:

使用拋出InterruptedException異常來控制線程的中斷。

5. 線程的睡眠和恢復:

  • Thread類的 sleep() 方法 : 此方法接收一個整數作爲參數,表示線程暫停運行的毫秒數。 在調用sleep() 方法後,當時間結束時,當JVM安排他們CPU時間,線程會繼續按指令執行。
  • TimeUnit枚舉類型的sleep() 方法: 它把接收的參數轉換成毫秒調用線程類的 sleep() 方法讓當前線程睡眠。

6. 等待線程的終結:

  • Thread 類的join() 方法: 當前線程調用某個線程的這個方法時,它會暫停當前線程,直到被調用線程執行完成。
  • 帶參數的join()方法:

join (long milliseconds)
join (long milliseconds, long nanos)

第一種join() 方法, 這方法讓調用線程等待特定的毫秒數。例如,如果thread1對象使用代碼thread2.join(1000), 那麼線程 thread1暫停運行,直到以下其中一個條件發生:
        thread2 結束運行
        1000 毫秒過去了
當其中一個條件爲真時,join() 方法返回。
第二個版本的 join() 方法和第一個很像,只不過它接收一個毫秒數和一個納秒數作爲參數。

7. 守護線程的創建和運行:

Java有一種特別的線程叫做守護線程。這種線程的優先級非常低,通常在程序裏沒有其他線程運行時纔會執行它。當守護線程是程序裏唯一在運行的線程時,JVM會結束守護線程並終止程序。

根據這些特點,守護線程通常用於在同一程序裏給普通線程(也叫用戶線程)提供服務。它們通常無限循環的等待服務請求或執行線程任務。它們不能做重要的任務,因爲我們不知道什麼時候會被分配到CPU時間片,並且只要沒有其他線程在運行,它們可能隨時被終止。JAVA中最典型的這種類型代表就是垃圾回收器。

只能在start() 方法之前可以調用 setDaemon() 方法。一旦線程運行了,就不能修改守護狀態。
可以使用 isDaemon() 方法來檢查線程是否是守護線程(方法返回 true) 或者是用戶線程 (方法返回 false)。

8. 在線程裏處理不受控制的異常:

Java裏有2種異常:

  • 檢查異常(Checked exceptions): 這些異常必須強制捕獲它們或在一個方法裏的throws子句中。 例如, IOException 或者ClassNotFoundException。
  • 非檢查異常(Unchecked exceptions): 這些異常不用強制捕獲它們。例如, NumberFormatException。
在一個線程 對象的 run() 方法裏拋出一個檢查異常,我們必須捕獲並處理他們。因爲 run() 方法不接受 throws 子句。當一個非檢查異常被拋出,默認的行爲是在控制檯寫下stack trace並退出程序。

幸運的是, Java 提供我們一種機制可以捕獲和處理線程對象拋出的非檢查異常來避免程序終結。

實現一個類來處理非檢查異常。這個類必須實現 UncaughtExceptionHandler 接口並實現在接口內已聲明的uncaughtException() 方法。

public class ExceptionHandler implements UncaughtExceptionHandler{
    public void uncaughtException(Thread t, Throwable e) {
      System.out.printf("An exception has been captured\n");
      System.out.printf("Thread: %s\n",t.getId());
      System.out.printf("Exception: %s: %s\n",e.getClass().getName(),e.getMessage());
      System.out.printf("Stack Trace: \n");
      e.printStackTrace(System.out); System.out.printf("Thread status: %s\n",t.getState());
    }
}

使用 setUncaughtExceptionHandler() 方法設置非檢查異常 handler 。

thread.setUncaughtExceptionHandler(new ExceptionHandler())
當在一個線程裏拋出一個異常,但是這個異常沒有被捕獲(這肯定是非檢查異常), JVM 檢查線程的相關方法是否有設置一個未捕捉異常的處理者 。如果有,JVM 使用Thread 對象和 Exception 作爲參數調用此方法 。如果線程沒有設置非檢查異常的處理者, 那麼 JVM會把異常的 stack trace 寫入操控臺並結束任務。

setDefaultUncaughtExceptionHandler() 爲應用裏的所有線程對象建立異常 handler 。
當一個未捕捉的異常在線程裏被拋出,JVM會尋找此異常的3種可能潛在的處理者(handler)。
首先, 它尋找這個未捕捉異常所在線程對象的異常handler。如果這個handler 不存在,那麼JVM會在線程對象的ThreadGroup裏尋找非捕捉異常的handler。如果此方法不存在,那麼 JVM 會尋找默認非捕捉異常handler。
如果沒有一個handler存在, 那麼 JVM會把異常的 stack trace 寫入操控臺並結束任務。

9. 使用本地線程變量ThreadLocal:

本地線程變量爲每個使用這些變量的線程儲存屬性值。可以用 get() 方法讀取值和使用 set() 方法改變值。 如果第一次你訪問本地線程變量的值,如果沒有值給當前的線程對象,那麼本地線程變量會調用 initialValue() 方法來設置值給線程並返回初始值。

    private static ThreadLocal<Date> startDate= new ThreadLocal<Date>() {
        protected Date initialValue(){
            return new Date();
        }
    };

本地線程類還提供 remove() 方法,刪除存儲在線程本地變量裏的值。

Java 併發 API 包括 InheritableThreadLocal 類提供線程創建線程的值的繼承性 。如果線程A有一個本地線程變量,然後它創建了另一個線程B,那麼線程B將有與A相同的本地線程變量值。 你可以覆蓋 childValue() 方法來初始子線程的本地線程變量的值。 它接收父線程的本地線程變量作爲參數。

10. 線程組:

Java併發 API裏有個有趣的方法是把線程分組。這個方法允許我們按線程組作爲一個單位來處理。例如,你有一些線程做着同樣的任務,你想控制他們,無論多少線程還在運行,他們的狀態會被一個調用中斷。

Java 提供 ThreadGroup 類來組織線程。 ThreadGroup 對象可以由 Thread 對象組成和由另外的 ThreadGroup 對象組成,生成線程樹結構。

當你調用Thread 類的構造函數時,傳遞它作爲ThreadGroup對象的第一個參數。

Thread thread = new Thread(threadGroup, searchTask);
使用list()方法打印關於ThreadGroup對象信息。
使用 activeCount()方法來獲取活躍的線程個數
使用enumerate()方法來獲取與ThreadGroup對象關聯的活躍線程的列表。

用interrupt() 方法中斷線程組中的所有線程。

ThreadGroup 類儲存線程對象和其他有關聯的 ThreadGroup 對象,所以它可以訪問他們的所有信息 (例如,狀態) 和執行全部成員的方法 (例如,中斷)。

11. 處理線程組內的不受控制異常:

對於編程語言來說,一個非常重要的事情是提供管理應用出錯情況的機制。Java 語言, 作爲最現代的編程語言,實現了一個基於異常的機制來管理出錯情況,它提供很多種類來表示不同的錯誤。當檢測到一個異常狀況時,這些異常會被Java類們拋出。你也可以使用這些異常, 或者實現你自己的異常, 來管理你的類產生的錯誤。
Java 也提供機制來捕捉和處理這些異常 。有些一定要被捕捉或者使用方法的throws句組再拋出,這些異常稱爲檢查異常(checked exceptions)。有些異常不需要被捕捉,這些稱爲非檢查異常(unchecked exceptions)。

當一個非捕獲異常在線程內拋出,JVM會爲這個異常尋找3種可能handlers。
首先, 它尋找這個未捕獲的線程對象的異常handler,如果這個handler 不存在,那麼JVM會在線程對象的ThreadGroup裏尋找非捕獲異常的handler,如果此方法不存在,那麼 JVM 會尋找默認非捕獲異常handler。如果沒有 handlers存在, 那麼 JVM會把異常的 stack trace 寫入控制檯並結束任務。

12. 用線程工廠創建線程:

在面向對象編程的世界中,工廠模式是最有用的設計模式。它是一個創造模式,還有它的目的是創建一個或幾個類的對象的對象。然後,當我們想創建這些類的對象時,我們使用工廠來代替new操作。線程工廠就是一個工廠模式。
有了這個工廠,我們有這些優勢來集中創建對象們:

  • 更簡單的改變了類的對象創建或者說創建這些對象的方式。
  • 更簡單的爲了限制的資源限制了對象的創建。 例如, 我們只new一個此類型的對象。
  • 更簡單的生成創建對象的統計數據。
Java提供一個接口ThreadFactory接口去實現一個線程對象工廠。 一些高級java併發API工具類就使用了線程工廠來創建線程。


參考資料:《Java 7 Concurrency Cookbook》

                 《Java 9 Concurrency Cookbook Second Edition》


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