线程是我们日常开发工作中,经常用到的一种技术,通过利用多线程技术,来提高服务器资源的利用率,从而达到提高程序运行效率的目的。多线程作为应用程序与底层系统资源交互的一种手段,在高级语言中(类似java),线程相关的api
比较少,而且多是偏向底层的native
方法。很多线程相关的实现细节,作为上层应用开发者,是感知不到的。正常情况下,程序正常运行,我们无需关注过多细节,但是当程序出现了异常,而且是关于线程内的异常时,该怎么处理呢?近几天小编就遇到了关于线程的异常处理问题,在解决问题的过程中,对线程相关的异常处理机制,也有了更深的了解。
你是如何处理线程异常的?
多线程的开发范式,通常如下:
Thread t = new Thread(new Runnable() {
@Override
public void run() {
// ....
}
});
t.start();
将需要在线程中执行的业务逻辑,定义到实现了Runnable
接口的的run方法中。
对于异常的处理,我们会在run方法中采用try{} catch(){}
的方式进行异常捕获和处理。
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try{
// ....
} catch(Exception e) {
// ....
}finally {
// ....
}
}
});
t.start();
但是结合上一篇 “你真的会处理异常吗” 中的介绍:有些场景下的异常处理,需要结合上下文,在产生异常的方法内,可能无法处理异常,产生的异常,需要让上层感知,在调用该方法的上层方法中进行异常的处理。
所以,子线程的run
方法中产生异常时,我们并不能直接将其捕获。而且,我们也不可能在所有使用多线程的地方,对run
方法上都添加try{}catch(){}
代码块,这样会让我们的代码变得很臃肿,除此之外,也不是所有的run方法都会抛出异常,需要我们来处理。
UncaughtExceptionHandler
那么当线程中产生了异常,而我们又没有对其进行捕获的情况下,异常会给谁来处理呢?
在jdk
设计之初,也考虑到了这个问题,通过 UncaughtExceptionHandler
来完善对线程异常的处理机制。
UncaughtExceptionHandler
定义如下:
@FunctionalInterface
public interface UncaughtExceptionHandler {
/**
* Method invoked when the given thread terminates due to the
* given uncaught exception.
* <p>Any exception thrown by this method will be ignored by the
* Java Virtual Machine.
* @param t the thread
* @param e the exception
*/
void uncaughtException(Thread t, Throwable e);
}
通过该接口的名称,可以大概知道它要做的事情:对没有被捕获的异常进行处理 。也就是说,如果在子线程中产生了异常,且没有捕获的情况下,异常会被 UncaughtExceptionHandler
进行处理。
UncaughtExceptionHandler如何捕获异常
其实UncaughtExceptionHandler
本身是无法捕获异常的,它只是对被捕获到的异常进行处理,也就是异常的处理逻辑定义在UncaughtExceptionHandler
中。
线程是一个操作系统层面的概念,java层面的Thread
类,仅仅是对线程的一个封装,在Thread
类中大部分方法都是native
的,所以Thread抛出的异常,其实是系统级别线程抛出的异常,这个异常,java语言层面是无法捕获的。
在UncaughtExceptionHandler
的注释中,也大概告诉我们一些异常的捕获流程:
/**
* Interface for handlers invoked when a <tt>Thread</tt> abruptly
* terminates due to an uncaught exception.
* <p>When a thread is about to terminate due to an uncaught exception
* the Java Virtual Machine will query the thread for its
* <tt>UncaughtExceptionHandler</tt> using
* {@link #getUncaughtExceptionHandler} and will invoke the handler's
* <tt>uncaughtException</tt> method, passing the thread and the
* exception as arguments.
* If a thread has not had its <tt>UncaughtExceptionHandler</tt>
* explicitly set, then its <tt>ThreadGroup</tt> object acts as its
* <tt>UncaughtExceptionHandler</tt>. If the <tt>ThreadGroup</tt> object
* has no
* special requirements for dealing with the exception, it can forward
* the invocation to the {@linkplain #getDefaultUncaughtExceptionHandler
* default uncaught exception handler}.
*
* @see #setDefaultUncaughtExceptionHandler
* @see #setUncaughtExceptionHandler
* @see ThreadGroup#uncaughtException
* @since 1.5
*/
这段注释的意思大概是:当一个线程由于未捕获的异常,而将要终止时,jvm会捕获到这个异常,然后按照一定的规则找到一个处理该异常的 UncaughtExceptionHandler
,并把产生异常的thread和异常实例,作为参数来调用 UncaughtExceptionHandler
中的uncaughtException
方法。
这里先做下小结:线程的常规异常处理方式:使用try{}catch(){}
将run
方法包裹起来,有些场景下,是无法满足我们的需求。为了解决这个问题,jdk
提供了UncaughtExceptionHandler
机制来处理:对于run
方法抛出的异常,会被jvm进行捕获,然后交由对应的UncaughtExceptionHandler
来处理。最终实现了,线程内产生了异常,通过jvm
捕获,然后分派到某个UncaughtExceptionHandler
中来处理。
哪个 UncaughtExceptionHandler 来处理异常
当jvm
捕获到了异常,将异常分派给哪个UncaughtExceptionHandler
呢?
在说分派流程前,我们先了解一下,线程中有哪些 UncaughtExceptionHandler
,可以被使用:在Thread
中有两个UncaughtExceptionHandler
类型的变量:
// null unless explicitly set
private volatile UncaughtExceptionHandler uncaughtExceptionHandler;
// null unless explicitly set
private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
一个是 静态类型的变量 defaultUncaughtExceptionHandler
,一个是实例变量 uncaughtExceptionHandler
,静态类型的变量是对所有线程实例都生效的,而实例属性仅仅对某个Thread
实例有效。
看到这两个变量,是不是有些陌生,平时使用多线程的时候,基本上很少用到过这两个变量,那UncaughtExceptionHandler
是如何生效的呢?
别急,这里要引出 线程组的概念了。
线程组
线程组,可以理解成一组线程的管理者,每个线程都有一个所属的组。使用线程组可以对组内的线程进行统一管理,如线程的停止,挂起等。其中最重要的是线程组自身实现了 UncaughtExceptionHandler接口,线程组内的线程可以使用所属的线程组,进行异常的异常处理。
线程组的一些核心代码如下:
public
class ThreadGroup implements Thread.UncaughtExceptionHandler {
private final ThreadGroup parent;
String name;
int maxPriority;
boolean destroyed;
boolean daemon;
boolean vmAllowSuspension;
int nthreads;
Thread threads[];// 线程组中的线程
// ...
/**
* Creates an empty Thread group that is not in any Thread group.
* This method is used to create the system Thread group.
*/
private ThreadGroup() { // called from C code
this.name = "system";
this.maxPriority = Thread.MAX_PRIORITY;
this.parent = null;
}
public void uncaughtException(Thread t, Throwable e) {
if (parent != null) {
parent.uncaughtException(t, e);
} else {
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler();
if (ueh != null) {
ueh.uncaughtException(t, e);
} else if (!(e instanceof ThreadDeath)) {
System.err.print("Exception in thread \""
+ t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
}
看到线程组是不是也有些陌生,没错,线程组这个类,平常也很少用到,创建线程的时候,也没有指定过线程组。那这个线程组,又是从哪里来的呢?
继续翻代码:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
//使用父线程的线程组
if (g == null) {
g = parent.getThreadGroup();
}
}
//...
/* Set thread ID */
tid = nextThreadID();
}
初始化线程的时候,如果没有指定线程组的话,那么被初始化的线程的线程组就是"父线程
"的线程组。
其实这里的父线程,仅仅是执行线程初始化方法的那个线程,和被创建的线程,并没有真正意义的父子关系。不先进程那样,子进程挂了,还有有父线程来回收。
那问题又来了,父线程的线程组,又是哪里来的? 按照上面说的父线程机制,可以逐层往上找,直到找到主线程,而主线程的线程组是jvm启动的时候指定的,这个涉及到了比较多的jvm底层知识,我们先不做深究,你知道主线程的线程组是一个名称为system
的线程组就可以了。
所以到这里,一个线程的 uncaughtExceptionHandler
可以有三个来源,第一个是实例自身指定的,第二个是defaultUncaughtExceptionHandler,第三个是线程组。
那这三个 uncaughtExceptionHandler
在处理异常时候的优先级是怎样的? 老规矩,看一下源码:
/**
* Dispatch an uncaught exception to the handler. This method is
* intended to be called only by the JVM.
*/
private void dispatchUncaughtException(Throwable e) {
getUncaughtExceptionHandler().uncaughtException(this, e);
}
当线程抛异常后,被jvm
捕获,jvm
调用方法dispatchUncaughtException
,来完成异常处理器的分派和异常的处理。具体分派逻辑如下:
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
这里可以看出:首先尝试使用 Thread实例的异常处理器,如果thread实例没有指定异常处理器的话,那么就使用线程组,作为异常处理器(线程组实现了 UncaughtExceptionHandler
),
接下来,我们具体看一下线程组是如何处理异常的:
public void uncaughtException(Thread t, Throwable e) {
if (parent != null) {
parent.uncaughtException(t, e);
} else {
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler();
if (ueh != null) {
ueh.uncaughtException(t, e);
} else if (!(e instanceof ThreadDeath)) {
System.err.print("Exception in thread \""
+ t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
}
在线程组中,优先使用其父线程组
进行异常的处理,线程组的父线程组类似于,线程的父线程一样,就是创建该线程组的线程的线程组,说起来比较绕,看一下代码就明白了:
public ThreadGroup(String name) {
// 使用当前线程的线程组
this(Thread.currentThread().getThreadGroup(), name);
}
public ThreadGroup(ThreadGroup parent, String name) {
this(checkParentAccess(parent), parent, name);
}
如果线程的线程组没有重写uncaughtException
方法的话,那么这里找父线程组的逻辑就会层层递进,会找到主线程的线程组,主线程的线程组名称为 system
,而system
线程组的父线程组为null
,所以接下来尝试使用 defaultUncaughtExceptionHandler
进行异常的处理,如果defaultUncaughtExceptionHandler
线程也不存在的话,那么最后的处理逻辑,就是使用错误流,将异常信息输出。也就是我们经常在控制台看到的一大坨异常堆栈信息。
总结
这里总结一下,在多线程环境中,如果没有使用try{}catch(){}显示的对异常进行捕获的话,线程的异常处理会交给UncaughtExceptionHandler 来完成。而在线程异常处理流程中存在三个 uncaughtExceptionHander:Thread实例变量uncaughtExceptionHander,Thrad的默认异常处理器defaultuncaughtExceptionHander和线程的线程组。这三个异常处理器在异常处理过程中的优先级如下:
Thread实例的 uncaughtExceptionHander
> 父线程组
>defaultuncaughtExceptionHander
到这里,大概已经知道了 java
中线程异常的处理方式。其实在工作中,我们使用多线程时,更多的是以线程池的方式使用,那么在使用线程池的时候,对异常的处理方式和线程的异常处理方式一样吗? 这个我们放在下一篇文章中学习。