从零开始学习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()方法以前设置。

相关文章
相关标签/搜索