生产问题(2) ExceptionInInitializerError导致线程池中的线程异常被吞

  1. 问题描述
  2. ExceptionInInitializerError
  3. FutureTask
  4. 修复方法
    1. 捕获Throwable而不是Exception
    2. 使用execute提交任务到线程池而不是submit
    3. 消费 Future
  5. 参考资料

最近在生产环境遇到一个比较极端的线程池吞异常问题,研究了下背后的原理,发现是静态块初始化异常抛出 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 三者的关系:

throwable-exception-error.png

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

参考资料


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 duval1024@gmail.com

×

喜欢就点赞,疼爱就打赏