最近在生产环境遇到一个比较极端的线程池吞异常问题,研究了下背后的原理,发现是静态块初始化异常抛出 ExceptionInInitializerError 导致的。这情景平时少见,在这里记录下已备忘。
问题描述
吞异常代码核心思想提炼后的样例是这样的:
@Slf4j
public class ExceptionSingleton {
private ExceptionSingleton() {
// 单例实例化过程中抛出运行时异常
throw new RuntimeException("ExceptionSingleton constructor exception.");
}
private static class SingletonHolder {
// 懒汉式单例
private volatile static ExceptionSingleton INSTANCE = new ExceptionSingleton();
}
public static ExceptionSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
static class Processor implements Runnable {
@Override
public void run() {
try {
ExceptionSingleton.getInstance();
} catch (Exception e) {
// 此处尝试捕获单例构造过程中抛出的RuntimeException,但其实无效
log.error("can not catch this exception here", e);
}
}
}
public static void main(String[] args) throws InterruptedException, TimeoutException, ExecutionException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(new Processor(););
Thread.currentThread().join();
}
}
这段代码的期望思路是在线程池线程中捕获单例实例化所抛出的 RuntimeException,并打印日志。实际执行结果并没有打印任何日志,提交到线程池的 Processor 仿佛是凭空消失了一般。
要深究背后的原因,我们先来探讨几个知识点:
ExceptionInInitializerError
从类注释可以看出来,ExceptionInInitializerError 在静态类变量或者静态块初始化的时候会被抛出:
/** * Signals that an unexpected exception has occurred in a static initializer. * An <code>ExceptionInInitializerError</code> is thrown to indicate that an * exception occurred during evaluation of a static initializer or the * initializer for a static variable. * * <p>As of release 1.4, this exception has been retrofitted to conform to * the general purpose exception-chaining mechanism. The "saved throwable * object" that may be provided at construction time and accessed via * the {@link #getException()} method is now known as the <i>cause</i>, * and may be accessed via the {@link Throwable#getCause()} method, as well * as the aforementioned "legacy method." * * @author Frank Yellin * @since JDK1.1 */ public class ExceptionInInitializerError extends LinkageError { /** * This field holds the exception if the * ExceptionInInitializerError(Throwable thrown) constructor was * used to instantiate the object * * @serial * */ private Throwable exception; // ... }
还需要注意在静态类变量或者静态块初始化中所抛出的所有异常,都需要使用 ExceptionInInitializerError 进行包装。特别注意的是:如果是抛出的是 RuntimeException,则JDK会自动使用 ExceptionInInitializerError 进行包装。
所以,最开始样例的单例抛出的 RuntimeException,其实被JDK包装成了 ExceptionInInitializerError。那么 Processor 内部的 try catch 块捕获的是 Exception 而不是 Throwable,那当然就不会打印异常日志 。不要忘记 Exception、Error 和 Throwable 三者的关系:
FutureTask
虽然 Processor 没有捕获 ExceptionInInitializerError,但为啥线程池内部也没打印相关错误日志呢?
这得深入看看线程池的代码。我们看到样例使用的提交方法是 submit:
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
submit 方法很简洁,一开始通过 newTaskFor 方法新建了一个 FutureTask,FutureTask 的第一个入参是用户提交的业务逻辑 runnable:
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}
runnable 会被缓存到内部字段 callable :
public class FutureTask<V> implements RunnableFuture<V> {
/** The underlying callable; nulled out after running */
private Callable<V> callable;
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
// ...
}
然后这个 ftask 会被提交到线程池队列中。紧接着,在线程池中会被取出ftask,并执行:
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 pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
// 执行task
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 {
// 调用afterExecute来做一些最后处理(比如可以打印执行异常)
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
task.run() 实际执行的是 FutureTask#run() 方法:
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);
}
}
callable 就是用户自定义的业务逻辑,如果在 callable 中抛出任何异常或者错误,都会在 try-catch 块中被捕获,并且通过 setException(ex) 方法,缓存到了字段 outcome 上:
/** The result to return or exception to throw from get() */
private Object outcome; // non-volatile, protected by state reads/writes
protected void setException(Throwable t) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = t;
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
finishCompletion();
}
}
因此,通过 submit 提交到线程池的 task 所抛出的异常,其实是被缓存到了 FutureTask 当中。
修复方法
通过以上的探讨,这个吞异常的问题可以有多个解决方法。
捕获Throwable而不是Exception
既然静态代码发生异常抛出的是 ExceptionInInitializerError ,它和 Exception 都是Throwable 的子类。因此,我们可以通过捕获 Throwable 来修复吞异常的问题:
static class Processor implements Runnable {
@Override
public void run() {
try {
ExceptionSingleton.getInstance();
} catch (Throwable e) {
log.error("catch throwable here", e);
}
}
}
使用execute提交任务到线程池而不是submit
通过 submit 方法提交的 task 会被自动包装为 FutureTask 而导致异常被缓存而不是直接抛出。但 execute 方法提交就不会进行包装,所以,改为 execute 方法也能修复问题:
executorService.execute(new Processor());
更进一步,我们从上边的 runWorker 方法可以注意到还可以重载 ThreadPoolExecutor 的 afterExecute 方法来打印异常信息,比如:
class MyThreadExecutor extends ThreadPoolExecutor {
// ...
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
// log throwable
}
}
消费 Future
这个问题样例在使用 submit 其实不够严谨。因为 summit 返回了一个 Future 实例,熟悉异步编程的话,应该知道我们应该消费掉这个 Future。所以可以这样子修复:
Future task = executorService.submit(new Processor());
task.get(500, TimeUnit.MILLISECONDS);
调用 FutureTask 的 get 方法,方法内部会检查到错误异常,并向外抛出。
Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.ExceptionInInitializerError
at java.util.concurrent.FutureTask.report(FutureTask.java:122)
at java.util.concurrent.FutureTask.get(FutureTask.java:206)
at org.demo.singleton.ExceptionSingleton.main(ExceptionSingleton.java:50)
Caused by: java.lang.ExceptionInInitializerError
at org.demo.singleton.ExceptionSingleton.getInstance(ExceptionSingleton.java:24)
at org.demo.singleton.ExceptionSingleton$Processor.run(ExceptionSingleton.java:32)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.RuntimeException: ExceptionSingleton constructor exception.
at org.demo.singleton.ExceptionSingleton.<init>(ExceptionSingleton.java:16)
at org.demo.singleton.ExceptionSingleton.<init>(ExceptionSingleton.java:12)
at org.demo.singleton.ExceptionSingleton$SingletonHolder.<clinit>(ExceptionSingleton.java:20)
... 7 more
参考资料
- When Does Java Throw the ExceptionInInitializerError?:介绍什么时候抛出ExceptionInInitializerError错误的好文章
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 duval1024@gmail.com