为什么我的异常日志没有打印出来?!

背景

最近在使用线程池的过程中,发现线程池执行的任务报错后没有错误日志打出来。纳尼?!线程池竟然还有这个坑?都说源码面前无秘密,所以我翻看了一下Java源码,客官请往下看:

源码分析

1. submit源码分析

由于这个不打印异常日志的问题是因为使用submit方法引起的,所以我们先看一下submit方法的源码:

1
2
3
4
5
6
7
8
9
10
/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}

我们可以看到这个方法把Runnable类型的任务转换成了Future类型的任务。我们知道,线程最后执行是通过调用run方法,那我们再深入看一下Future中的run方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
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 {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}

我们可以看到在Future的run方法中有个try-catch,所以异常在这里被处理了,这就是在我们仅仅使用submit方法,异常日志没有打印出来的原因。

实际上,通过submit提交的任务,可以通过返回的Future对象的get方法拿到结果,我们再跟踪一下Future的get方法:

1
2
3
4
5
6
7
8
9
/**
* @throws CancellationException {@inheritDoc}
*/
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}

我们可以看到最后调用了report方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Returns result or throws exception for completed task.
*
* @param s completed state value
*/
@SuppressWarnings("unchecked")
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);
}

这里我们可以看到如果线程的状态不是正常的,那么就会抛出异常,而这个异常正是在run的时候被捕获的异常对象。所以其实通过submit提交的任务也可以看到异常日志,只不过需要调用get方法才能看到。

2. execute源码分析

而用execute执行的任务在运行过程中报错是可以打印错误日志的,那我们再来看一下execute的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}

在这里并没有处理异常的地方,所以在执行任务的过程中如果发生异常会直接打印出来。

解决方法

如果我们只是提交任务,不管任务的执行结果,并且需要打印任务执行过程中的错误,那么我们可以就要使用execute方法。如果我们需要知道任务执行之后的结果,就要使用submit方法。

如果你非要使用submit,并且想看到运行过程中的错误日志,那么也不是没有办法。其中重写ThreadPoolExecutor的afterExecute方法就可以实现。比如这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MyThreadPoolExecutor extends ThreadPoolExecutor {
public MyThreadPoolExecutor(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) {
super.afterExecute(r, t);

if (t == null && r instanceof Future<?>) {
try {
Object result = ((Future<?>) r).get();
} catch (CancellationException ce) {
t = ce;
} catch (ExecutionException ee) {
t = ee.getCause();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // ignore/reset
}
}
if (t != null) {
t.printStackTrace();
}

}
}

这个afterExecute方法会在线程执行完后回调,所以有异常也会通过我们重写的方法打印日志出来。然后你直接使用你这个自定义的线程池并且使用submit方法在运行时碰到异常就可以打印出日来了。

总结

所以这其实也不是坑哈,只是我们在使用线程池的过程中没有对这两个方法做更多的了解,所以只要你理解了我上面的源码分析,你以后就知道在什么场合该用什么方法去提交任务了。谢谢客官观看!


为什么我的异常日志没有打印出来?!
https://www.chuckfang.com/2020/06/26/submit-execute/
作者
方程
发布于
2020年6月26日
许可协议