從零開始學習Java多線程(二)

前面已經簡單介紹進程和線程,爲後續學習作鋪墊。本文討論多線程傳參,Java多線程異常處理機制。java

1. 多線程的參數傳遞

     在傳統開發過程當中,咱們習慣在調用函數時,將所需的參數傳入其中,經過函數內部邏輯處理返回結果,大多狀況下,整個過程均是由一條線程執行,排除運行沒必要要的的偶發性,彷佛並不會出現意料以外的結果。而在多線程環境下,在使用線程時須要對線程進行一些必要的初始化,線程對這些數據進行處理後返回結果,因爲線程的運行和結束並不可控,線程傳參變得複雜起來,本文就以上問題介紹三種經常使用的傳遞參數方式。安全

 (一)構造方法傳參數據結構

      在建立線程時,須要建立一個Thread類的或者其子類的實例,經過調用其start()方法執行run()方法中的代碼塊,在此以前,咱們能夠經過構造函數傳遞線程運行所須要的數據,並使用變量保存起來。代碼以下:多線程

 1 public class MyThread extends Thread {
 2 
 3     //定義變量保存參數
 4     private String msg;
 5 
 6     public MyThread() {
 7     }
 8 
 9     public MyThread(String msg) {
10         this.msg = msg;
11     }
12 
13     //多線程的入口
14     public void run() {
15         System.out.println("MyThread " + msg);
16     }
17 
18     public static void main(String[] args) {
19         MyThread myThread = new MyThread("is running");
20         myThread.start();
21     }
22 }

運行結果:dom

MyThread is running

Process finished with exit code 0

     這種方式的優勢很明顯:簡單、安全。在線程運行以前數據已經準備完成,避免線程丟失數據,若是傳遞更復雜數據,能夠定義集合或者類等數據結構。缺點就是傳遞比較多的參數時,這種方式會使構造方法過於複雜,爲了不這種狀況能夠經過類方法和變量傳遞參數ide

(二)變量和類方法傳遞參數

     在Thread實例類中定義須要傳遞的參數變量,而且定義一系列public的方法(或變量),在建立完Tread實例後經過調用方法給參數逐個賦值。上面的代碼也能夠經過定義setMsg()方法傳遞參數,代碼以下:函數

 1 public class MyThread extends Thread {
 2 
 3     //定義變量保存參數
 4     private String msg;
 5 
 6     public void setMsg(String msg) {
 7         this.msg = msg;
 8     }
 9 
10     public MyThread() {
11     }
12 
13     public MyThread(String msg) {
14         this.msg = msg;
15     }
16 
17     //多線程的入口
18     public void run() {
19         System.out.println("MyThread " + msg);
20         System.out.println(Thread.currentThread().getThreadGroup());
21     }
22 
23     public static void main(String[] args) {
24         MyThread myThread = new MyThread();
25         myThread.setMsg("is running");
26         myThread.start();
27     }
28 }

(三)經過回調函數傳遞數據

     以上線程傳遞參數最經常使用的兩種方式,可是能夠發現參數都在main方法中設置,而後Thread實例被動的接受參數,假如在線程運行中動態的獲取參數,如在run()方法先獲取三個隨機數,經過Work類的process方法對這隨機數求和,最後經過Data類的value值返回結果。此例看出在返回value以前,由於隨機數的不肯定性,咱們並不能事先傳遞的值value。學習

 1 class Data {
 2     public int value = 0;
 3 }
 4 
 5 class Work {
 6     //回調函數
 7     public void process(Data data, Integer[] numbers) {
 8         for (int n : numbers) {
 9             data.value += n;
10         }
11     }
12 }
13 
14 public class MyThread extends Thread {
15     //回調函數的對象
16     private Work work;
17 
18     public MyThread(Work work) {
19         this.work = work;
20     }
21 
22     public void run() {
23         java.util.Random random = new java.util.Random();
24         Data data = new Data();
25         int n1 = random.nextInt(1000);
26         int n2 = random.nextInt(2000);
27         int n3 = random.nextInt(3000);
28         work.process(data, new Integer[]{n1, n2, n3}); // 使用回調函數
29         System.out.println(String.valueOf(n1) + "+" + String.valueOf(n2) + "+"
30                 + String.valueOf(n3) + "=" + data.value);
31     }
32 
33     public static void main(String[] args) {
34         Thread thread = new MyThread(new Work());
35         thread.start();
36     }
37 
38 }
View Code

    上面代碼中process()方法便是回調函數,實質上是一個事件函數。整個事件流程爲:將求和對象work傳入線程,線程執行過程當中三個隨機數的產生觸發了求和事件,經過傳遞進來的work對象調用process()方法,最後將結果返回線程並在控制檯輸出。這種傳遞數據的方式是在上面兩種傳參的基礎上進行了薄層封裝,並無直接將參數傳遞給線程,而是經過傳遞的對象進行邏輯處理以後將結果返回。this

2. Java多線程異常處理機制

     先來看對於未檢查異常在run()方法中是如何處理的。spa

     

    運行結果:

     

     由上能夠看出對於未檢查異常,從線程將會直接宕掉,主線程繼續運行。那麼在主線程中能不能捕獲到異常呢?咱們直接將所有代碼塊try catch起來

     

     而後你會發現並無什麼卵用,主線程沒有捕獲到任何異常信息,和未檢出異常一模一樣,從線程直接宕掉,主線程繼續運行

     若是先檢查異常呢?

      

     IDEA提示:

     

     結果仍是讓人失望,程序還沒運行,IDEA已經提示報錯,原來run()方法自己不支持拋出異常的,方法重寫更不容許throws,因此run()方法不支持往外拋出異常。

     最後再試下能不能在run()方法中try catch捕獲異常,

      

      運行結果:

     

      原來在run()方法中try catch是能捕捉到異常的。因此對於多線程已檢查的異常咱們能夠經過try catch進行處理,而對於未檢查的異常,若是沒有處理,一旦拋出該線程立馬宕掉,主線程則繼續運行,那麼未檢查的異常也所有須要try catch嗎?固然這也是一種方式。除此以外,Java還提供了異常處理器。

  (一)Java異常處理器

    在Java線程run()方法中,對於未檢查異常,藉助於異常處理器進行處理。異常處理器能夠直接理解爲異常處理的方法,下面爲具體如何使用。

    UncaughtExceptionHandler,是Thread的內部接口。

   

  Thread內部有兩個變量,用來記錄異常處理器。

  

   Thread內部也分別提供了它們的get/set方法,set()方法其實沒什麼特別,主要是用來設置這兩個內部變量,重點在於它們的get()方法。

   對於getUncaughtExceptionHandler方法,若是當前UncaughtExceptionHandler對象不爲空,那麼直接返回該對象,若是爲空,返回該線程所屬的線程組,由此得知,ThreadGroup實現了UncaughtExceptionHandler接口。

  

  

  

   與此同時,內部實現了uncaughtException方法,

  

   而對於getDefaultUncaughtExceptionHandler()方法,只是簡單的返回內部對象。

   

    至此,咱們能夠了解到Thread內部有兩個異常處理器,分別提供了get/set方法,對於set方法只是單純的設置異常處理器,對於get方法,getDefaultUncaughtExceptionHandler()方法直接獲取處理器;getUncaughtExceptionHandler()方法,進行判空,若是非空直接返回,若是爲空返回該線程所屬的線程組,而且當前線程組是實現了Thread.UncaughtExceptionHandler接口,內部實現了public void uncaughtException(Thread t, Throwable e),其本質上它纔是線程處理器。

    對於defaultUncaughtExceptionHandler,表示應該程序默認的,整個程序可使用的,它的get/set方法均爲static修飾;對於uncaughtExceptionHandler,屬於實例方法,也就是說每一個線程能夠擁有一個,簡言之:每一個線程均可以有一個uncaughtExceptionHandler,整個應用能夠有一個defaultUncaughtExceptionHandler。它們之間是個體與全局的關係,若是個體擁有那麼就再也不使用全局的;不然,走全局。這樣作的好處是很是靈活,既能夠保證單個線程特別處理,又能夠保障整個程序作到統一處理,在諸多場景發揮多種用處。

  (二)異常處理邏輯

      當run()方法中發生異常,JVM調用異常分發器,也就是藉助getUncaughtExceptionHandler方法獲取異常處理器,而後執行它的uncaughtException方法。

     

     因此關鍵之處在於uncaughtException()方法,再來看它的源碼:

 1 public void uncaughtException(Thread t, Throwable e) {
 2         //是否存在父線程組
 3         if (parent != null) {
 4             parent.uncaughtException(t, e);
 5         } else {
 6             //獲取默認異常處理器
 7             Thread.UncaughtExceptionHandler ueh =
 8                     Thread.getDefaultUncaughtExceptionHandler();
 9             //若是非空,調用其uncaughtException方法
10             if (ueh != null) {
11                 ueh.uncaughtException(t, e);
12                 //不然打出標準錯誤信息
13             } else if (!(e instanceof ThreadDeath)) {
14                 System.err.print("Exception in thread \""
15                         + t.getName() + "\" ");
16                 e.printStackTrace(System.err);
17             }
18         }
19     }
View Code

    若是已經設置異常處理器,那麼直接返回,若是沒有設置,返回當前線程組,而且調用線程組的uncaughtException方法時(如上圖),若是該線程組重寫了uncaughtException方法,直接調用;若是沒有,調用該線程組的父線程組;若是父線程組仍然沒有重寫,調用爺爺線程組,以此類推。可是若是全部的線程組都沒有重寫,進入else裏面,在else中獲取默認處理器,若是默認有,執行uncaughtException方法,若是沒有直接system.err。

   以上就是異常處理器的處理邏輯,能夠看出uncaughtExceptionHandler處理器優先級要高於defaultUncaughtException,這也符合就近原則,若是自身擁有了,又何須調用全局擁有的呢!

  (三)異常處理器代碼演示

 1 public class TestThread {
 2 
 3     public static void main(String[] args) {
 4             MyThread myThread = new MyThread();
 5             myThread.setUncaughtExceptionHandler((Thread t,Throwable e)->{
 6                 System.out.println("出現異常...");
 7                 System.out.println("線程名稱: "+myThread.getName());
 8                 System.out.println("異常信息: "+e.getMessage());
 9             });
10             myThread.start();
11             System.out.println("main runing....");
12             System.out.println("main runing....");
13             System.out.println("main runing....");
14             System.out.println("main runing....");
15     }
16 }
17 class MyThread extends Thread {
18     public void run() {
19             //拋出異常
20             int i = 13 / 0;
21     }
22 }
View Code

  運行結果:

   

  以上示例能夠看出,儘管爲檢查異常,經過異常處理器依然可以感知異常和信息獲取,不會直接宕掉了。主要注意的是,必須在調用start()方法以前設置異常處理器,不然線程依舊直接宕掉。

 (四)總結

      Java多線程異常處理機制依賴於Thread內部兩個異常處理器,uncaughtExceptionHandler和defaultUncaughtExceptionHandler。二者之間爲個體和全局之間的關係,若是前者已經設置,那麼直接使用,不然使用後者。大體步驟爲:

      a. 若是設置了異常處理器uncaughtExceptionHandler直接使用。

      b. 若是沒設置,將會在祖先線程組中查找第一個重寫了uncaughtException的線程組,而後調用他的uncaughtException方法

      c. 若是都沒有重寫,那麼使用應用默認的全局異常處理器defaultUncaughtExceptionHandler

      d. 若是仍是沒有設置,直接標準錯誤打印信息

       若是想要設置線程特有的異常處理器,能夠調用set方法進行設置;若是想要對全局進行設置,能夠調用靜態方法進行設置,須要注意的是必需要在調用start()方法以前設置。

相關文章
相關標籤/搜索