11

抓虫笔记-ExceptionInInitializerError导致线程池中的线程异常被吞

 3 years ago
source link: https://blog.duval.top/2020/12/15/%E6%8A%93%E8%99%AB%E7%AC%94%E8%AE%B0-ExceptionInInitializerError%E5%AF%BC%E8%87%B4%E7%BA%BF%E7%A8%8B%E6%B1%A0%E4%B8%AD%E7%9A%84%E7%BA%BF%E7%A8%8B%E5%BC%82%E5%B8%B8%E8%A2%AB%E5%90%9E/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

抓虫笔记-ExceptionInInitializerError导致线程池中的线程异常被吞

2020-12-15 抓虫笔记

7.3k 7 分钟

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

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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK