前面已经简单介绍进程和线程,为后续学习做铺垫。本文讨论多线程传参,Java多线程异常处理机制。

1. 多线程的参数传递

在传统开发过程中,我们习惯在调用函数时,将所需的参数传入其中,通过函数内部逻辑处理返回结果,大多情况下,整个过程均是由一条线程执行,排除运行不必要的的偶发性,似乎并不会出现意料之外的结果。而在多线程环境下,在使用线程时需要对线程进行一些必要的初始化,线程对这些数据进行处理后返回结果,由于线程的运行和结束并不可控,线程传参变得复杂起来,本文就以上问题介绍三种常用的传递参数方式。

一)构造方法传参

在创建线程时,需要创建一个Thread类的或者其子类的实例,通过调用其start()方法执行run()方法中的代码块,在此之前,我们可以通过构造函数传递线程运行所需要的数据,并使用变量保存起来。代码如下:

 public class MyThread extends Thread {

     //定义变量保存参数
private String msg; public MyThread() {
} public MyThread(String msg) {
this.msg = msg;
} //多线程的入口
public void run() {
System.out.println("MyThread " + msg);
} public static void main(String[] args) {
MyThread myThread = new MyThread("is running");
myThread.start();
}
}

运行结果:

MyThread is running

Process finished with exit code 0

这种方式的优点很明显:简单、安全。在线程运行之前数据已经准备完成,避免线程丢失数据,如果传递更复杂数据,可以定义集合或者类等数据结构。缺点就是传递比较多的参数时,这种方式会使构造方法过于复杂,为了避免这种情况可以通过类方法和变量传递参数

(二)变量和类方法传递参数

在Thread实例类中定义需要传递的参数变量,并且定义一系列public的方法(或变量),在创建完Tread实例后通过调用方法给参数逐个赋值。上面的代码也可以通过定义setMsg()方法传递参数,代码如下:

 public class MyThread extends Thread {

     //定义变量保存参数
private String msg; public void setMsg(String msg) {
this.msg = msg;
} public MyThread() {
} public MyThread(String msg) {
this.msg = msg;
} //多线程的入口
public void run() {
System.out.println("MyThread " + msg);
System.out.println(Thread.currentThread().getThreadGroup());
} public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.setMsg("is running");
myThread.start();
}
}

(三)通过回调函数传递数据

以上线程传递参数最常用的两种方式,但是可以发现参数都在main方法中设置,然后Thread实例被动的接受参数,假如在线程运行中动态的获取参数,如在run()方法先获取三个随机数,通过Work类的process方法对这随机数求和,最后通过Data类的value值返回结果。此例看出在返回value之前,因为随机数的不确定性,我们并不能事先传递的值value。

 class Data {
public int value = 0;
} class Work {
//回调函数
public void process(Data data, Integer[] numbers) {
for (int n : numbers) {
data.value += n;
}
}
} public class MyThread extends Thread {
//回调函数的对象
private Work work; public MyThread(Work work) {
this.work = work;
} public void run() {
java.util.Random random = new java.util.Random();
Data data = new Data();
int n1 = random.nextInt(1000);
int n2 = random.nextInt(2000);
int n3 = random.nextInt(3000);
work.process(data, new Integer[]{n1, n2, n3}); // 使用回调函数
System.out.println(String.valueOf(n1) + "+" + String.valueOf(n2) + "+"
+ String.valueOf(n3) + "=" + data.value);
} public static void main(String[] args) {
Thread thread = new MyThread(new Work());
thread.start();
} }

上面代码中process()方法即是回调函数,实质上是一个事件函数。整个事件流程为:将求和对象work传入线程,线程执行过程中三个随机数的产生触发了求和事件,通过传递进来的work对象调用process()方法,最后将结果返回线程并在控制台输出。这种传递数据的方式是在上面两种传参的基础上进行了薄层封装,并没有直接将参数传递给线程,而是通过传递的对象进行逻辑处理之后将结果返回。

2. Java多线程异常处理机制

先来看对于未检查异常在run()方法中是如何处理的。

运行结果:

由上可以看出对于未检查异常,从线程将会直接宕掉,主线程继续运行。那么在主线程中能不能捕获到异常呢?我们直接将全部代码块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()方法,再来看它的源码:

 public void uncaughtException(Thread t, Throwable e) {
//是否存在父线程组
if (parent != null) {
parent.uncaughtException(t, e);
} else {
//获取默认异常处理器
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler();
//如果非空,调用其uncaughtException方法
if (ueh != null) {
ueh.uncaughtException(t, e);
//否则打出标准错误信息
} else if (!(e instanceof ThreadDeath)) {
System.err.print("Exception in thread \""
+ t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
}

如果已经设置异常处理器,那么直接返回,如果没有设置,返回当前线程组,并且调用线程组的uncaughtException方法时(如上图),如果该线程组重写了uncaughtException方法,直接调用;如果没有,调用该线程组的父线程组;如果父线程组仍然没有重写,调用爷爷线程组,以此类推。但是如果所有的线程组都没有重写,进入else里面,在else中获取默认处理器,如果默认有,执行uncaughtException方法,如果没有直接system.err。

以上就是异常处理器的处理逻辑,可以看出uncaughtExceptionHandler处理器优先级要高于defaultUncaughtException,这也符合就近原则,如果自身拥有了,又何必调用全局拥有的呢!

(三)异常处理器代码演示

 public class TestThread {

     public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.setUncaughtExceptionHandler((Thread t,Throwable e)->{
System.out.println("出现异常...");
System.out.println("线程名称: "+myThread.getName());
System.out.println("异常信息: "+e.getMessage());
});
myThread.start();
System.out.println("main runing....");
System.out.println("main runing....");
System.out.println("main runing....");
System.out.println("main runing....");
}
}
class MyThread extends Thread {
public void run() {
//抛出异常
int i = 13 / 0;
}
}

运行结果:

以上示例可以看出,尽管为检查异常,通过异常处理器依然能够感知异常和信息获取,不会直接宕掉了。主要注意的是,必须在调用start()方法之前设置异常处理器,否则线程依旧直接宕掉。

(四)总结

Java多线程异常处理机制依赖于Thread内部两个异常处理器,uncaughtExceptionHandler和defaultUncaughtExceptionHandler。两者之间为个体和全局之间的关系,如果前者已经设置,那么直接使用,否则使用后者。大致步骤为:

a. 如果设置了异常处理器uncaughtExceptionHandler直接使用。

b. 如果没设置,将会在祖先线程组中查找第一个重写了uncaughtException的线程组,然后调用他的uncaughtException方法

c. 如果都没有重写,那么使用应用默认的全局异常处理器defaultUncaughtExceptionHandler

d. 如果还是没有设置,直接标准错误打印信息

如果想要设置线程特有的异常处理器,可以调用set方法进行设置;如果想要对全局进行设置,可以调用静态方法进行设置,需要注意的是必须要在调用start()方法之前设置。

从零开始学习Java多线程(二)的更多相关文章

  1. 从零开始学习Java多线程(一)

    1. 什么是进程? 对其概念需要自行goole,简单理解就是:进程是计算机系统进行资源分配和调度的基本单位,是正在运行程序的实体:每一个进程都有它自己的内存空间和系统资源:进程是线程的容器.如:打开I ...

  2. 从零开始学习Java多线程(三)

    本文主要对Java多线程同步与通信以及相关锁的介绍. 1 .Java多线程安全问题 Java多线程安全问题是实现并发最大的问题,可以说多线程开发其实就是围绕多线程安全问题开发,涉及之深,不是简简单单一 ...

  3. 从零开始学习JAVA(入门基础)

    目录 博主从零开始学习JAVA(入门基础) 1.搭建JAVA开发环境 卸载JDK(未安装的请忽略) 安装JDK 2.编程语言中,何为编译型与解释型 编译型 解释型 3.第一个JAVA应用程序 4.JA ...

  4. java 多线程二

    java 多线程一 java 多线程二 java 多线程三 java 多线程四 线程中断: /** * Created by root on 17-9-30. */ public class Test ...

  5. 从零开始学习java一般需要多长时间?

    从零开始学习java一般需要多长时间? 其实学java一般要多久?因人而异,例如一个零基础的小白自学java,每天学习8个小时来算,而且在有学习资料的基础上,每天学习,从零到找到工作,起码要半年起步, ...

  6. 从火箭发场景来学习Java多线程并发闭锁对象

    从火箭发场景来学习Java多线程并发闭锁对象 倒计时器场景 在我们开发过程中,有时候会使用到倒计时计数器.最简单的是:int size = 5; 执行后,size—这种方式来实现.但是在多线程并发的情 ...

  7. Java多线程(二) —— 深入剖析ThreadLocal

    对Java多线程中的ThreadLocal类还不是很了解,所以在此总结一下. 主要参考了http://www.cnblogs.com/dolphin0520/p/3920407.html 中的文章. ...

  8. 我也学习JAVA多线程-join

    在工作中,挺少遇到join关键字,但很多多线程资料和面试过程中,初中级开发工程师总会遇到join. 今天一起学习下join. join的作用:等待指定的时间(当为0时,一直等待),直到这个线程执行结束 ...

  9. java多线程二之线程同步的三种方法

          java多线程的难点是在:处理多个线程同步与并发运行时线程间的通信问题.java在处理线程同步时,常用方法有: 1.synchronized关键字. 2.Lock显示加锁. 3.信号量Se ...

随机推荐

  1. CentOS 7 Squid代理服务器反向代理

    Squid反向代理 将不同的URL请求分发到后台不同的Web服务器上,通过squid反向代理,可以加速网站的访问速度,同时互联网用户只能看到反向代理服务器的地址,加强网站的访问安全 Squid反向代理 ...

  2. 279. Perfect Squares(动态规划)

    Given a positive integer n, find the least number of perfect square numbers (for example, 1, 4, 9, 1 ...

  3. 第02节:JMS基本概念和模型

    1.JMS是什么 JMS Java Message Service,Java消息服务,是Java EE中的一个技术. 2.JMS规范 JMS定义了Java中访问消息中间件的接口,并没有机遇实现,实现J ...

  4. sys模块

    #python run_case.py #在terminal中执行.py文件 在terminal中执行.py文件: 注: 无法使用terminal来打开那个文件 会显示如下:python: can't ...

  5. 叮咚,你的Lauce上线了!

    哈,2014 - 2016 - 2018,虽然每隔两年才有那么一篇随笔,博客园,我还是爱你的~ 嗯,2018,马上又要失业了,我这是自带黑属性啊啊啊哈,工作了4年多的项目要被砍掉了, 倒不是说非要这个 ...

  6. Literal绑定数据

    前台: <asp:Literal ID = "ChiCunShow" runat = "server"></asp:Literal> 后 ...

  7. Prometheus监控学习笔记之Prometheus从1.x升级到2.x

    详细参考这篇文章 https://cloud.tencent.com/developer/article/1171434 prometheus 2.0于2017-11-08发布,主要是存储引擎进行了优 ...

  8. bzoj2839 集合计数(容斥)

    2839: 集合计数 Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 883  Solved: 490[Submit][Status][Discuss] ...

  9. 解决 Invalid character found in method name. HTTP method names must be tokens 异常信息

    一个线上环境出现异常,不是spring boot. 解决方法: server.xml中加上maxHttpHeaderSize="8192" springBoot 1.x则是在app ...

  10. Star in Parentheses

    问题 A: Star in Parentheses 时间限制: 1 Sec  内存限制: 128 MB 题目描述 You are given a string S, which is balanced ...