文章目录
- 问题出现
- 问题解决
问题出现
简单逻辑描述:
用户下载模板上传的excel文件,然后填写后导入服务器,其中有一些字段有填写要求(例如A中填写1,则B只能填写1)。用户将excel文件导入服务器的时候,服务器基于多线程进行校验,如果校验失败则会抛出业务异常。然后将异常信息发给前台,进行提示。
现在问题是,线程抛出异常后没有被主线程拿到,而是直接被JVM处理了(在服务器终端打印堆栈信息)
我要做的就是,让主线程拿到子线程的异常信息,然后返回给前台
问题解决
以上这个问题如果复习过线程、线程池相关的资料,一眼就能能看出来,这是一个“子线程异常传递”的问题。
幸好之前总结过这类问题,没想到居然在公司改BUG的时候用到了
【1】给线程布置任务有两个接口,一个是runnable(对应run方法),另一个是callable(对应call方法),前者声明的时候没有抛出异常(throws Exception),而后者声明了异常。
通过这次改BUG最直观的感受就是:
run方法中,如果遇到编译期异常(例如IOException)一定要在run方法内部完成处理,不能够向上抛出。
if(isMatch){
throw new IOException("导入数据不符合规范");
}
而要该的代码中,抛出的就是一个受检异常。
虽然run方法的checked exception必须处理,但是runtime exception是可以抛出的,那么我是不是可以把IO exception的message取出来,重新包装为一个runtimeException。
catch (InvocationTargetException e) {
//拿到造成反射调用异常内部的异常
Throwable exception = e.getTargetException();
//将IOException作为非受检异常抛出去,最终JVM会回调异常线程的UncaughtExceptionHandler.uncaughtException()
if(exception instanceof IOException){
IOException ioException = (IOException) exception;
System.out.println("生产者抛出异常");
throw new IllegalThreadStateException(ioException.getMessage());
}
}
注意:这里用于解析用户导入的excel这个方法read()不是直接调用的,而是通过反射调用的,因此最终捕获的不是read()抛出的IOException,而是经过封装之后的InvocationTargetException。我们可以通过getTargetException拿到内部的异常对象。
【2】
想清楚一个问题,一旦一个子线程映射到一个操作系统线程之后,他和主线程就已经分道扬镳了,完全是两个执行流,即使子线程死亡也丝毫不影响主线程的运行。(这里仅限于操作系统内核线程实现的情况)
因此,即使子线程抛出了异常,主线程也无法拿到它,这个异常仅会被JVM(可以看作java虚拟机层面的操作系统)单独处理。
我们需要做的就是为子线程和父线程建立连接、建立通信。
本代码中的线程都是new出来的,不是使用线程池的方法,所以和线程池相关异常处理方法肯定是需要排除了。
futureTask实例的get() 和 继承线程池,重写钩子方法afterExecute()
现在考虑最后一种常见的方法——为进程设置未捕获异常处理器
在Thread中,Java提供了一个setUncaughtExceptionHandler的方法来设置线程的异常处理函数,你可以把异常处理函数传进去,当发生线程的未捕获异常的时候,由JVM来回调执行,默认没有设置回调函数的时候,JVM执行默认行为——将堆栈信息使用system.out.err进行打印
fileProducerThread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
APP.this.exception = e.getMessage(); //这里是内部类,调用外部的成员变量
}
});
第一步解决的问题是:让子线程把异常抛出去,而这一步则是“子线程的异常抛给JVM后,如何进行下一步处理”,我们设置未捕获异常处理器回答这个问题。
这里的exception可以是一个线程安全的队列,但是此处业务只使用一个生产者线程,所以使用一个变量保存message,由于保存message的字符串是一个共享变量,出于保守的原因,这里使用同步块修饰共享变量。
synchronized (this){
if(this.exception!=null){
HashMap<String, Object> map = new HashMap<>();
map.put("producerException",this.exception);
response.add(map);
this.exception=null;
}
}
此时一旦子线程抛出异常后,就可以被主线程获取到。(这里的程序中用到了countdownLatch,主线程会调用await()等待子线程依次完成countdown()调用)
总结:JVM可以看作主线程与子线程的桥梁,子线程将运行时异常抛给JVM,JVM回调子线程的未捕获异常处理器,将异常信息保存到共享变量(例如队列)中,主线程可以从共享变量拿到子线程抛出的异常。其实本质上还是线程通信问题