在实际开发中,我们常常会用到线程池,但任务一旦提交到线程池之后,如果发生异常之后,怎么处理? 怎么获取到异常信息?在了解这个问题之前,可以先看一下 线程池的源码解析,从链接中我们知道了线程池的提交方式:submit和execute的区别,接下来分别使用他们执行带有异常的任务!看结果是怎么样的!
我们先用伪代码模拟一下线程池抛异常的场景:
public class ThreadPoolException { public static void main (String[] args) { //创建一个线程池
ExecutorService executorService= Executors.newFixedThreadPool(1 ); //当线程池抛出异常后 submit无提示,其他线程继续执行 executorService.submit(new task()); //当线程池抛出异常后 execute抛出异常,其他线程继续执行新任务 executorService.execute(new task()); } }//任务类 class task implements Runnable { @Override public void run () { System.out.println("进入了task方法!!!" ); int i=1 /0 ; } }
运行结果:
可以看到:submit不打印异常信息,而execute则会打印异常信息!,submit的方式不打印异常信息,显然在生产中,是不可行的,因为我们无法保证线程中的任务永不异常,而如果使用submit的方式出现了异常,直接如上写法,我们将无法获取到异常信息,做出对应的判断和处理,所以下一步需要知道如何获取线程池抛出的异常!
submit()
想要获取异常信息就必须使用
get()
方法!!
//当线程池抛出异常后 submit无提示,其他线程继续执行 Future> submit = executorService.submit(new task()); submit.get();
submit打印异常信息如下:
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/ruoyi-vue-pro
视频教程:https://doc.iocoder.cn/video/
public class ThreadPoolException { public static void main (String[] args) { //创建一个线程池 ExecutorService executorService = Executors.newFixedThreadPool(1 ); //当线程池抛出异常后 submit无提示,其他线程继续执行 executorService.submit(new task()); //当线程池抛出异常后 execute抛出异常,其他线程继续执行新任务 executorService.execute(new task()); } }// 任务类 class task implements Runnable { @Override public void run () { try { System.out.println("进入了task方法!!!" ); int i = 1 / 0 ; } catch (Exception e) { System.out.println("使用了try -catch 捕获异常" + e); } } }
打印结果:
可以看到 submit 和 execute都清晰易懂的捕获到了异常,可以知道我们的任务出现了问题,而不是消失的无影无踪。
方案一中,每一个任务都要加一个
try-catch
实在是太麻烦了,而且代码也不好看,那么这样想的话,可以用
Thread.setDefaultUncaughtExceptionHandler
方法捕获异常
UncaughtExceptionHandler
是Thread类一个内部类,也是一个函数式接口。
内部的
uncaughtException
是一个处理线程内发生的异常的方法,参数为线程对象t和异常对象e。
应用在线程池中如下所示:重写它的线程工厂方法,在线程工厂创建线程的时候,都赋予
UncaughtExceptionHandler
处理器对象。
public class ThreadPoolException { public static void main (String[] args) throws InterruptedException { //1.实现一个自己的线程池工厂 ThreadFactory factory = (Runnable r) -> { //创建一个线程 Thread t = new Thread(r); //给创建的线程设置UncaughtExceptionHandler对象 里面实现异常的默认逻辑 t.setDefaultUncaughtExceptionHandler((Thread thread1, Throwable e) -> { System.out.println("线程工厂设置的exceptionHandler" + e.getMessage()); }); return t; }; //2.创建一个自己定义的线程池,使用自己定义的线程工厂 ExecutorService executorService = new ThreadPoolExecutor( 1 , 1 , 0 , TimeUnit.MILLISECONDS, new
LinkedBlockingQueue(10 ), factory); // submit无提示 executorService.submit(new task()); Thread.sleep(1000 ); System.out.println("==================为检验打印结果,1秒后执行execute方法" ); // execute 方法被线程工厂factory 的UncaughtExceptionHandler捕捉到异常 executorService.execute(new task()); } }class task implements Runnable { @Override public void run () { System.out.println("进入了task方法!!!" ); int i = 1 / 0 ; } }
打印结果如下:
根据打印结果我们看到,execute方法被线程工厂factory中设置的
UncaughtExceptionHandler
捕捉到异常,而submit方法却没有任何反应!说明
UncaughtExceptionHandler
在submit中并没有被调用。这是为什么呢?
在日常使用中,我们知道,execute和submit最大的区别就是execute没有返回值,submit有返回值。submit返回的是一个future ,可以通过这个future取到线程执行的结果或者异常信息。
Future> submit = executorService.submit(new task());//打印异常结果 System.out.println(submit.get());
从结果看出:submit并不是丢失了异常,使用
future.get()
还是有异常打印的!!那为什么线程工厂factory 的
UncaughtExceptionHandler
没有打印异常呢?猜测是submit方法内部已经捕获了异常, 只是没有打印出来,也因为异常已经被捕获,因此jvm也就不会去调用Thread的
UncaughtExceptionHandler
去处理异常。
接下来,验证猜想:
首先看一下submit和execute的源码:
execute方法的源码在这博客中写的很详细,点击查看execute源码,在此就不再啰嗦了
https://blog.csdn.net/qq_45076180/article/details/108316340
submit源码在底层还是调用的execute方法,只不过多一层Future封装,并返回了这个Future,这也解释了为什么submit会有返回值
//submit()方法 public Future submit (Callable task) { if (task == null ) throw new NullPointerException(); //execute内部执行这个对象内部的逻辑,然后将结果或者异常 set到这个ftask里面 RunnableFuture ftask = newTaskFor(task); // 执行execute方法 execute(ftask); //返回这个ftask return ftask; }
可以看到submit也是调用的execute,在execute方法中,我们的任务被提交到了
addWorker(command, true)
,然后为每一个任务创建一个Worker去处理这个线程,这个Worker也是一个线程,执行任务时调用的就是Worker的run方法!run方法内部又调用了runworker方法!如下所示:
public void run () { runWorker(this ); } final void runWorker (Worker w) { Thread wt = Thread.currentThread(); Runnable task = w.firstTask; w.firstTask = null ; w.unlock(); // allow interrupts boolean completedAbruptly = true ; try { //这里就是线程可以重用的原因,循环+条件判断,不断从队列中取任务 //还有一个问题就是非核心线程的超时删除是怎么解决的 //主要就是getTask方法()见下文③ while (task != null || (task = getTask()) != null ) { w.lock(); if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) wt.interrupt(); try { beforeExecute(wt, task); Throwable thrown = null ; try { //执行线程 task.run(); //异常处理 } catch (RuntimeException x) { thrown = x; throw x; } catch (Error x) { thrown = x; throw x; } catch (Throwable x) { thrown = x; throw new Error(x); } finally { //execute的方式可以重写此方法处理异常 afterExecute(task, thrown); } } finally { task = null ; w.completedTasks++; w.unlock(); } } //出现异常时completedAbruptly不会被修改为false completedAbruptly = false ; } finally { //如果如果completedAbruptly值为true,则出现异常,则添加新的Worker处理后边的线程 processWorkerExit(w, completedAbruptly); } }
核心就在
task.run();
这个方法里面了, 期间如果发生异常会被抛出。
如果用execute提交的任务,会被封装成了一个runable任务,然后进去 再被封装成一个worker,最后在worker的run方法里面调用runWoker方法,
runWoker
方法里面执行任务任务,如果任务出现异常,用
try-catch
捕获异常往外面抛,我们在最外层使用
try-catch
捕获到了
runWoker
方法中抛出的异常。因此我们在execute中看到了我们的任务的异常信息。
那么为什么submit没有异常信息呢? 因为submit是将任务封装成了一个
futureTask
,然后这个
futureTask
被封装成worker,在woker的run方法里面,最终调用的是
futureTask
的run方法, 猜测里面是直接吞掉了异常,并没有抛出异常,因此在worker的
runWorker
方法里面无法捕获到异常。
下面来看一下
futureTask
的run方法,果不其然,在try-catch中吞掉了异常,将异常放到了
setException(ex);
里面
public void run () { if (state != NEW || !UNSAFE.compareAndSwapObject(this , runnerOffset, null , Thread.currentThread())) return ; try { Callable c = callable; if (c != null && state == NEW) { V result; boolean ran; try { result = c.call(); ran = true ; } catch (Throwable ex) { result = null ; ran = false ; //在此方法中设置了异常信息 setException(ex); } if (ran) set(result); } //省略下文 。。。。。。 setException(ex)`方法如下:将异常对象赋予`outcomeprotected void setException (Throwable t) { if (UNSAFE.compareAndSwapInt(this , stateOffset, NEW, COMPLETING)) { //将异常对象赋予outcome,记住这个outcome, outcome = t; UNSAFE.putOrderedInt(this , stateOffset, EXCEPTIONAL); // final state finishCompletion(); } }
将异常对象赋予
outcome
有什么用呢?这个
outcome
是什么呢?当我们使用submit返回Future对象,并使用
Future.get()
时, 会调用内部的report方法!
public V get () throws InterruptedException, ExecutionException { int s = state; if (s <= COMPLETING) s = awaitDone(false , 0L ); //注意这个方法 return report(s); }
reoport里面实际上返回的是outcome ,刚好之前的异常就set到了这个outcome里面
private V report (int s) throws ExecutionException { //设置`outcome` Object x = outcome; if (s == NORMAL) //返回`outcome` return (V)x; if (s >= CANCELLED) throw new CancellationException(); throw new ExecutionException((Throwable)x); }
因此,在用submit提交的时候,runable对象被封装成了future ,future 里面的 run方法在处理异常时,
try-catch
了所有的异常,通过
setException(ex);
方法设置到了变量outcome里面, 可以通过
future.get
获取到outcome。
所以在submit提交的时候,里面发生了异常, 是不会有任何抛出信息的。而通过
future.get()
可以获取到submit抛出的异常!在submit里面,除了从返回结果里面取到异常之外, 没有其他方法。因此,在不需要返回结果的情况下,最好用execute ,这样就算没有写
try-catch
,疏漏了异常捕捉,也不至于丢掉异常信息。
通过上述源码分析,在excute的方法里面,可以通过重写
afterExecute
进行异常处理,但是注意! 这个也只适用于excute提交(submit的方式比较麻烦,下面说),因为submit的
task.run
里面把异常吞了,根本不会跑出来异常,因此也不会有异常进入到a
fterExecute
里面。
在
runWorker
里面,调用task.run之后,会调用线程池的
afterExecute(task, thrown)
方法
final void runWorker (Worker w) {//当前线程 Thread wt = Thread.currentThread(); //我们的提交的任务 Runnable task = w.firstTask; w.firstTask = null ; w.unlock(); // allow interrupts boolean completedAbruptly = true ; try { while (task != null || (task = getTask()) != null ) { w.lock(); if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) wt.interrupt(); try { beforeExecute(wt, task); Throwable thrown = null ; try