之前写过几个线程,线程A不停的扫描(while (true))指定目录下有没有新的binlog日志文件产生,线程B每晚执行定时任务,删除三天前产生的日志文件。当线程A通过类File读取指定目录和文件时,线程B刚好执行删除任务,线程A会抛出IOException, 导致线程A逻辑出错,结果线程A不是一直扫描(RUNNING),而是一直停留在WAIT状态。先分析一下,为什么停留在WAIT状态。
下面是模拟一个业务上的简单代码:
public class ChildProcessTest {
private static final ExecutorService offlineFileProcess =
Executors.newFixedThreadPool(4);
static final Logger LOGGER = LoggerFactory
.getLogger(ChildProcessTest.class);
public static void main(String[] args)
throws ExecutionException, InterruptedException {
Future<Object> future = offlineFileProcess.submit(() -> {
Thread.currentThread().setName("scanBinLogFileProducer");
while (true) {
// try {
getFileCreateTime("E:\\test\\test.txt");
// illegalDivision();
// } catch (Exception e) {
// LOGGER.error(e.getMessage(), e);
// }
}
});
System.out.println(future.get());
}
}
public class FileInfoUtil {
public static Long getFileCreateTime(String dirPath)
throws IOException {
File logFile = new File(dirPath);
Long fileCreateTime = Files.readAttributes(logFile.toPath(),
BasicFileAttributes.class).creationTime().toMillis();
return fileCreateTime;
}
public static void illegalDivision() {
int i = 1/0;
}
}
启动main方法,线程scanBinLogFileProducer开始执行,执行方法里面有个where(true), 该线程将一直处于RUNNING状态。当将本地文件E:\\test\\test.txt 删除时,按照我们的预期,方法getFileCreateTime会抛出异常如下:
但是并没有,为什么呢?因为我们提交线程用的是submit方法, 它用FutureTask进行封装,真正的方法执行是:FutureTask.run()。如下图所示:
上图中的2处将方法的异常进行了捕获(如果是线程池的execute方法,将会将异常向外抛出),然后通过方法setException将异常信息设置到FutureTask里面,可以通过其get方法获取异常信息。重点来了!虽然2处将异常捕获了,但是该任务(1处)的执行也到此结束了,所以线程将处于WAIT状态。如果在线程的任务方法里面自己捕获异常处理,这里就不会执行到上图的2,3,那么本例中的where(true)将会进行一直执行,线程将一直处于RUNNING状态。submit和execute的区别也可以参考文章:线程池中线程抛了异常如何处理?-CSDN博客
现在,我们就简单讨论一下,子线程抛出异常如何处理。
首先明确线程代码的边界。其实很简单,Runnable接口的run方法所界定的边界就可以看作是线程代码的边界。Runnable接口中run方法原型如下:
public void run();
而所有的具体线程都实现这个方法,所以这里就明确了一点,线程代码不能抛出任何checked异常。所有的线程中的checked异常都只能被线程本身消化掉。:) 这样本身也是符合线程的设计理念的,线程本身就是被看作独立的执行片断,它应该对自己负责,所以由它来消化所有的checked异常是很正常的。因此,我们首先确定:checked异常一定要在线程内部消化,最常用的方式,就是try{...}catch(..){...}。
但是,线程代码中是可以抛出错误(Error)和运行级别异常(RuntimeException)的。Error俺们可以忽略,因为通常Error是应该留给jvm的,而RuntimeException确是比较正常的,如果在运行过程中满足了某种条件导致线程必须中断,可以选择使用抛出运行级别异常来处理,如下:
public void run() {
if (...) throw new RuntimeException();
}
当线程代码抛出运行级别异常之后,线程会中断。:)这点java中解释得很清楚:
@see Thread
All threads that are not daemon threads have died, either by returning from the call to the run method or “by throwing an exception that propagates beyond the run method”.
但是对于invoke此线程的主线程会产生什么影响呢?主线程不受这个影响,不会处理这个RuntimeException,而且根本不能catch到这个异常。会继续执行自己的代码 :)
所以得到结论:线程方法的异常只能自己来处理。
但是,给某个thread设置一个UncaughtExceptionHandler,可以确保在该线程出现异常时能通过回调UncaughtExceptionHandler接口的public void uncaughtException(Thread t, Throwable e) 方法来处理异常,这样的好处或者说目的是可以在线程代码边界之外(Thread的run()方法之外),有一个地方能处理未捕获异常。但是要特别明确的是:虽然是在回调方法中处理异常,但这个回调方法在执行时依然还在抛出异常的这个线程中!另外还要特别说明一点:如果线程是通过线程池创建,线程异常发生时UncaughtExceptionHandler接口不一定会立即回调。代码示例如下:
package study20170103;
/**
* Created by apple on 17/1/3.
*/
public class ThreadTest extends ThreadGroup{
private ThreadTest(){
super("ThreadTest");
}
public static void main(String[] args) {
new Thread(new ThreadTest(),new Runnable() {//传入继承ThreadGroup的类对象
@Override
public void run() {
throw new NullPointerException();//只能抛出unchecked异常
}
}).start();
}
public void uncaughtException(Thread thread, Throwable exception) {
/**
* 当线程抛出unckecked异常时,系统会自动调用该函数,但是是在抛出异常的线程内执行
*/
System.out.println(thread.getId());
exception.printStackTrace();//example, print stacktrace
}
}
比之上述方法,还有一种编程上的处理方式可以借鉴,即,有时候主线程的调用方可能只是想知道子线程执行过程中发生过哪些异常,而不一定会处理或是立即处理,那么发起子线程的方法可以把子线程抛出的异常实例收集起来作为一个Exception的List返回给调用方,由调用方来根据异常情况决定如何应对。不过要特别注意的是,此时子线程早以终结。
线程设计的理念:“线程的问题应该线程自己本身来解决,而不要委托到外部。”