天天看点

公司项目改BUG:子线程异常信息无法传递到主线程问题出现问题解决

文章目录

  • 问题出现
  • 问题解决

问题出现

简单逻辑描述:

用户下载模板上传的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回调子线程的未捕获异常处理器,将异常信息保存到共享变量(例如队列)中,主线程可以从共享变量拿到子线程抛出的异常。其实本质上还是线程通信问题