一、问题分析

我们来看一下java中的线程池是如何运行我们提交的任务的,详细流程比较复杂,这里我们不关注,我们只关注任务执行的部分。java中的线程池用的是ThreadPoolExecutor,真正执行代码的部分是runWorker方法:final void runWorker(Worker w)

 

可以看到,程序会捕获包括Error在内的所有异常,并且在程序最后,将出现过的异常和当前任务传递给afterExecute方法。

而ThreadPoolExecutor中的afterExecute方法是没有任何实现的。

protected void afterExecute(Runnable r, Throwable t) { }

二、存在问题

想象下ThreadPoolExecutor这种处理方式会有什么问题?

这样做能够保证我们提交的任务抛出了异常不会影响其他任务的执行,同时也不会对用来执行该任务的线程产生任何影响。

问题就在afterExecute方法上, 这个方法没有做任何处理,所以如果我们的任务抛出了异常,我们也无法立刻感知到。 即使感知到了,也无法查看异常信息。

所以,作为一名好的开发者,是不应该允许这种情况出现的。

如何避免这种问题?思路很简单。。。

  • 在提交的任务中将异常捕获并处理,不抛给线程池。
  • 异常抛给线程池,但是我们要及时处理抛出的异常。

1、直接catch

第一种思路很简单,就是我们提交任务的时候,将所有可能的异常都Catch住,并且自己处理。

说白了就是把业务逻辑都trycatch起来。 但是这种思路的缺点就是:

  • 所有的不同任务类型都要trycatch,增加了代码量。
  • 不存在checkedexception的地方也需要都trycatch起来,代码丑陋。

2、线程池实现

第二种思路就可以避免上面的两个问题。

第二种思路又有以下四种实现方式

(1)自定义线程池

自定义线程池,继承ThreadPoolExecutor并复写其afterExecute(Runnable r, Throwable t)方法。

 

(2)实现Thread.UncaughtExceptionHandler接口

实现Thread.UncaughtExceptionHandler接口,实现void uncaughtException(Thread t, Throwable e);方法,并将该handler传递给线程池的ThreadFactory

 

(3)继承ThreadGroup

覆盖其uncaughtException方法。(与第二种方式类似,因为ThreadGroup类本身就实现了Thread.UncaughtExceptionHandler接口)

尤其注意:上面三种方式针对的都是通过execute(xx)的方式提交任务,如果你提交任务用的是submit()方法,那么上面的三种方式都将不起作用,而应该使用下面的方式

 

(4)采用Future模式

如果提交任务的时候使用的方法是submit,那么该方法将返回一个Future对象,所有的异常以及处理结果都可以通过future对象获取。 采用Future模式,将返回结果以及异常放到Future中,在Future中处理

 

三、总结

异常处理是java中非常重要的流程,但是线程池的默认操作,会使的这些内容被静悄悄的忽略,这在某些情况下是致命的。

本文探讨了从用户层面的代码到线程池层面的各种改造方法,力求让业务代码更加健壮可控。