Java多线程异常捕获之UncaughtExceptionHandler

Posted by Yuanyeex on Wednesday, May 9, 2018

Thread和UncaughtExceptionHandler

相信现在大家在写java程序时,必然会接触到多线程的概念。多线程很强大,但是也很容易出错。其中一个经常被忽略的错误就是线程无故异常退出,这种情况多数是因为某些未被捕获的异常直接抛出导致的,而且这时候如果处理不当,可能会导致系统资源的泄露,比如数据库连接未释放等。

先看下面的例子:

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        int a = 1 / 0;
        System.out.println(a);
    }
};

new Thread(runnable).start();

上面的代码中,执行到1/0的时候必然会抛出异常,导致下面的语句没法执行。虽然run接口不抛出任何受检异常,但是确可能抛出未受检异常从而导致执行线程的退出。为了防止线程无声的退出,我们可以在代码中用try{...}catch(Throwable t){}的方法来讲所有的异常捕获。另一种方法是利用Thread提供的UncaughtExceptionHandler来处理:

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        int a = 1 / 0;
        System.out.println(a);
    }
};

Thread thread = new Thread(runnable);
thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println(String.format("Exception got - doing something, thread=%s, errMsg=%s", t.getName(), e.getMessage()));
    }
});
thread.start();

执行结果:

Exception got - doing something, thread=Thread-0, errMsg=/ by zero

在上面的代码中,我们为线程指定了一个Handler来处理未被捕获的异常,这种做法只会对该线程有限,其他的线程抛出的未受检异常则不会被处理。Thread中可以设置一个默认的UncaughtExceptionHandler,这样可以将该异常处理句柄应用到大部分的线程上。

Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        // do something
    }
});

Executor框架和UncaughtExceptionHandler

使用Thread.setDefaultUncaughtExceptionHandler

Executor框架是最常用的一个线程框架,那么在线程池中,如果线程抛出的异常未被捕获,同样会导致工作线程的退出,线程池会根据情况,确定是否起新的线程来代替该工作线程。前面提到的通过Thread.setDefaultUncaughtExceptionHandler同样对通过线程池创建的线程起作用:

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        int a = 1 / 0;
        System.out.println(a);
    }
};

Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("Uncaught exception, thread=" + t.getName() + ", exception=" + e.getMessage());
    }
});

ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 2; i++) {
    executorService.execute(runnable);
}

执行结果:

Uncaught exception, thread=pool-1-thread-1, exception=/ by zero
Uncaught exception, thread=pool-1-thread-2, exception=/ by zero

利用ThreadFactory

还有一种方法就是利用ThreadFactory,我们在创建线程池的时候指定一个ThreadFactory,该工厂负责为每个由其创建线程设置UncaughtExceptionHandler。

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("Start");
        int a = 1 / 0;
        System.out.println(a);
    }
};

Thread.UncaughtExceptionHandler uncaughtExceptionHandler = new Thread.UncaughtExceptionHandler() {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("Uncaught exception, thread=" + t.getName() + ", exception=" + e.getMessage());
    }
};

AtomicInteger threadCount = new AtomicInteger();
ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r, "thread-" + threadCount.incrementAndGet());
        thread.setUncaughtExceptionHandler(uncaughtExceptionHandler);
        return thread;
    }
});
for (int i = 0; i < 2; i++) {
    executorService.execute(runnable);
}

执行结果:

Start
Uncaught exception, thread=pool-1-thread-1, exception=/ by zero
Start
Uncaught exception, thread=pool-1-thread-2, exception=/ by zero

请注意submit(Runnable)

但是,即使你设置了UncaughtExceptionHandler,在线程池中也可能会遇到异常未被处理的情况。我们把上面的代码中线程池execute()改为submit()再执行一次:

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("Start");
        int a = 1 / 0;
        System.out.println(a);
    }
};

Thread.UncaughtExceptionHandler uncaughtExceptionHandler = new Thread.UncaughtExceptionHandler() {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("Uncaught exception, thread=" + t.getName() + ", exception=" + e.getMessage());
    }
};

AtomicInteger threadCount = new AtomicInteger();
ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r, "thread-" + threadCount.incrementAndGet());
        thread.setUncaughtExceptionHandler(uncaughtExceptionHandler);
        return thread;
    }
});
for (int i = 0; i < 2; i++) {
    executorService.execute(runnable);
}

输出:

Start
Start

可以发现,这时候异常并没有被UncaughtExceptionHandler处理。这是因为对于submit执行的任务,task产生的未处理的异常都会存在Future对象中作为执行的一种结果。我们可以通过Future#get(),如果有异常会被封装成ExecutionException抛出来:

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("Start");
        int a = 1 / 0;
        System.out.println("End");
    }
};

Thread.UncaughtExceptionHandler uncaughtExceptionHandler = new Thread.UncaughtExceptionHandler() {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("Uncaught exception, thread=" + t.getName() + ", exception=" + e.getMessage());
    }
};

AtomicInteger threadCount = new AtomicInteger();
ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r, "thread-" + threadCount.incrementAndGet());
        thread.setUncaughtExceptionHandler(uncaughtExceptionHandler);
        return thread;
    }
});
for (int i = 0; i < 2; i++) {
    Future future = executorService.submit(runnable);
    try {
        future.get();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        System.out.println("Exception from future " + e.getCause().getMessage());
    }
}

运行结果:

Start
Exception from future / by zero
Start
Exception from future / by zero

ScheduledExecutorService的execute和submit

这里还有个特例,ScheduledExecutorService的execute方法其实也是调用的submit,所以就算你调用ScheduledExecutorService#submit,任务中抛出的异常也不会走到UncaughtExceptionHandler中去。

// ScheduledThreadPoolExecutor#execute的源码
/**
 * Executes {@code command} with zero required delay.
 * This has effect equivalent to
 * {@link #schedule(Runnable,long,TimeUnit) schedule(command, 0, anyUnit)}.
 * Note that inspections of the queue and of the list returned by
 * {@code shutdownNow} will access the zero-delayed
 * {@link ScheduledFuture}, not the {@code command} itself.
 *
 * <p>A consequence of the use of {@code ScheduledFuture} objects is
 * that {@link ThreadPoolExecutor#afterExecute afterExecute} is always
 * called with a null second {@code Throwable} argument, even if the
 * {@code command} terminated abruptly.  Instead, the {@code Throwable}
 * thrown by such a task can be obtained via {@link Future#get}.
 *
 * @throws RejectedExecutionException at discretion of
 *         {@code RejectedExecutionHandler}, if the task
 *         cannot be accepted for execution because the
 *         executor has been shut down
 * @throws NullPointerException {@inheritDoc}
 */
public void execute(Runnable command) {
    schedule(command, 0, NANOSECONDS);
}

总结

线程的异常退出可能会导致一些诡异的错误,应该尽量保证异常都被处理到。UncaughtExceptionHandler是一种有效的手段,但是要注意在和Executor框架结合是,submit的task产生的未捕获的异常不会被注册的UncaughtExceptionHandler处理,需要通过Future#get来处理。需要注意的是ScheduledExecutorService#execute其实是调用的submit方法。

「真诚赞赏,手留余香」

Yuanyeex

真诚赞赏,手留余香

使用微信扫描二维码完成支付