ExecutorService加强了Executor这个接口,并提供了submit方法以加强Executor中的execute方法,也正是因为这两个方法某些细微的差异,造成了对异常处理时两个方法的千差万别。
在面对异常时,有如下几种场景
Thread | 1.自定义UncaughtExceptionHandler 2.用 try/catch代码块包围3.自己实现Thread的子类去记录异常日志 |
ThreadPoolExecutor | 1.重写afterExecute()方法来记录异常 2.检查Future |
Future/ FutureTask | 1.调用get方法以获取内部的执行异常 2.重写 FutureTask.done()方法 |
综上来看,异常处理无非三种方式,要么用try/catch,要么自定义个handler,要么去弄个子类覆盖某些方法,我们今天就来探索探索这里面的坑。
先用一段非常简单的代码来体会下execute和submit对于异常处理的不同
public static void main(String[] args) throws Exception {
ExecutorService exe = Executors.newFixedThreadPool(1);
//exe.execute(new Task());
exe.submit(new Task());
public static class Task implements Runnable {
@Override
public void run() {
System.out.println(Integer.parseInt("123"));
System.out.println(Integer.parseInt("XYZ"));//这里会抛RuntimeException
}
}
}
启动main方法,输出如下(注意!没有异常!)
submit木有异常!
接着注释掉submit,启用execute,则会抛出异常
输出123后抛出异常
我们再修改下main方法
public static void main(String[] args) throws Exception {
ExecutorService exe = Executors.newFixedThreadPool(1);
Future f=exe.submit(new Task());
f.get();
}
这样修改之后,程序也像execute方法那样抛出了异常,这也是这两者之间的差别,submit方法会把异常也当作是任务的一部分,只有调用get了才能拿得到异常。
为什么submit会“吞下”异常直至调用他的get方法呢,我把代码贴出来,就一目了然了。
我们所调用的submit方法,是在AbstractExecutorService中实现的
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);//将任务包装一下
execute(ftask); //执行任务
return ftask;
}
execute方法是在ThreadPoolExecutor中实现的,整个execute方法其实就是判断线程池的状态然后选择到底是new新线程执行还是加入队列等等,干事的就是这一句addWorker(command, true);
之后就会调用内部类Worker的run方法,
我们具体看下这个方法
final void runWorker(Worker w) {//省略了一些状态判断的代码
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run(); //此处调用FutureTask.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 {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
}
接着FutureTask.run()
public void run() {//省略了一些状态判断的代码
try {
Callable<V> 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);
}
} finally {
//省略了其他代码
}
}
这样,异常就被submit给含在嘴里了,要他吐出来,只能去调用他的get方法
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s); //接着往下看
}
private V report(int s) throws ExecutionException {
Object x = outcome;
if (s == NORMAL)
return (V)x;
if (s >= CANCELLED)
throw new CancellationException();
throw new ExecutionException((Throwable)x);//异常直到这里才会抛出
}
而如果是execute方法的话,task.run调用的就是Callable或者Runnable的方法了,所以有异常就直接抛了,没有了那FutureTask那层包装,所以异常最终给了afterExecute(task, thrown)这个接收函数。
因此对于线程池采用execute()方法来执行线程任务时,要想捕获未处理的异常:
(1)可以重写afterExecute()方法来实现:
public class ThreadStudy2 {
public static class ExceptionCaughtThreadPool extends ThreadPoolExecutor {
List<Throwable> exceptions=new LinkedList();
public ExceptionCaughtThreadPool(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue){
super(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {//通过重写线程池的afterExecute函数也能捕获execute执行任务时的异常
if (t!=null){
exceptions.add(t);
System.out.println("catch exception in afterExecute");
return;
}
System.out.println("everything is ok");
}
}
public static void main(String[] args){
ExceptionCaughtThreadPool threadPool=new ExceptionCaughtThreadPool(1,1,100, TimeUnit.SECONDS,new LinkedBlockingQueue<>());
threadPool.execute(new Runnable() {
@Override
public void run() {
throw new RuntimeException();
}
});
System.out.println("--------DONE-----------");
}
}
(2)实现UncaughtExceptionHandler;如果你没有实现一个handler,那么他就使用默认的handler来处理异常:
public class ThreadStudy {
public static void main(String[] args) {
final Thread.UncaughtExceptionHandler exceptionHandler = new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
synchronized (this) {
System.err.println("Uncaught exception in thread '" + t.getName() + "': " + e.getMessage());
}
}
};
// 自定义线程的newThread方法以加入自己的Handler
ThreadFactory threadFactory = new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
System.out.println("creating pooled thread");
final Thread thread = new Thread(r);
//以为这样可以抓住执行过程的异常
thread.setUncaughtExceptionHandler(exceptionHandler);
return thread;
}
};
ExecutorService threadPool
= Executors.newFixedThreadPool(1, threadFactory);
// Callable callable = new Callable() {
// @Override
// public Integer call() throws Exception {
// throw new Exception("Error in Callable");
// }
// };
// threadPool.submit(callable);//submit执行时不能捕获UncaughtExceptionHandler异常
// threadPool.shutdown();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("执行任务");
int num = Integer.parseInt("TT");
}
};
threadPool.execute(runnable);//execute执行时能捕获未处理的异常
System.out.println("-----------Done.---------");
}
}
但是对于submit来说,异常是绑定到Future上了,但是调用future.get()的时候,这些异常才会给你抛出来,意味着你自己定义的handler其实是无效的;
结论:
如果我们关心线程池执行的结果,则需要使用submit来提交task,那么在afterExecute中对异常的处理也需要通过Future接口调用get方法去取结果,才能拿到异常,如果我们不关心这个任务的结果,可以直接使用ExecutorService中的execute方法(实际是继承Executor接口)来直接去执行任务,这样的话,我们的Runnable没有经过多余的封装,在runWorker中得到的异常也直接能在afterExecute中捕捉。
参考文档:
https://www.cnblogs.com/wscit/p/6100476.html 这篇文章讨论通过自定义线程工程类,设置UncaughtException对象来捕获执行execute方法时抛出的异常
https://www.jianshu.com/p/d7d0a32cf028 这篇文章中对于复写afterExecute方法处理异常的描述不正确,是可以获取到异常信息的。