前面的博客中主要描述的关于线程的概念,通过源码分析了解线程的基本操作方式,但是如何在线程运行期间获取异常信息呢?这就要使用到一个Hook线程了
线程运行时的异常
在Thread类中,关于线程运行时异常的API一共有四个如下图所示
UncaughtExceptionHandler
在线程执行的过程中是没有办法检查抛出异常的,这个是因为执行的方法没有返回值而且线程又运行在属于自己的上下文环境中,所以由它派生出的线程无法获取到它运行过程中的异常,所以Java提供了一个接口UncaughtExceptionHandler,当在运行过程中线程出现异常,就会调用UncaughtExceptionHandler这个接口,如下代码
@FunctionalInterface
public interface UncaughtExceptionHandler {
/**
* Method invoked when the given thread terminates due to the
* given uncaught exception.
* <p>Any exception thrown by this method will be ignored by the
* Java Virtual Machine.
* @param t the thread
* @param e the exception
*/
void uncaughtException(Thread t, Throwable e);
}
@FunctionalInterface表示它是一个函数式的接口并且接口中只有一个方法,那么在什么地方调用这个接口方法呢?
/**
* Dispatch an uncaught exception to the handler. This method is
* intended to be called only by the JVM.
*/
private void dispatchUncaughtException(Throwable e) {
getUncaughtExceptionHandler().uncaughtException(this, e);
}
在线程运行的过程中如果出现异常,则会调用dispatchUNcaughtException方法,而这个方法又会调用如下的方法
/**
* Returns the handler invoked when this thread abruptly terminates
* due to an uncaught exception. If this thread has not had an
* uncaught exception handler explicitly set then this thread's
* <tt>ThreadGroup</tt> object is returned, unless this thread
* has terminated, in which case <tt>null</tt> is returned.
* @since 1.5
* @return the uncaught exception handler for this thread
*/
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
首先判断了当前线程是否设置了Handler,如果有则将使用uncaughtExceptionHandler,如果没有的话就需要从Group中获取,而在ThreadGroup中也应该有同样的方法进行获取操作
public void uncaughtException(Thread t, Throwable e) {
if (parent != null) {
parent.uncaughtException(t, e);
} else {
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler();
if (ueh != null) {
ueh.uncaughtException(t, e);
} else if (!(e instanceof ThreadDeath)) {
System.err.print("Exception in thread \""
+ t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
}
- 这个方法首先判断是否有父的ThreadGroup,如果有的话就直接调用。
- 如果没有的话获取到全部默认的uncaughtExceptionHandler调用其中的uncaughtException方法
- 如果两个都没有的话就会将线程中的信息定向到System.err中。
示例代码
public class TestException {
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler((t,e)->{
System.out.println(t.getName()+" occur exception");
e.printStackTrace();
});
final Thread thread = new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(1/0);
},"Test1");
thread.start();
}
}
结果
uncaughtException异常调用关系图
什么是Hook
Java JVM进程退出时因为JVM进程管理中没有了活跃的非守护线程,或者收到系统中断的信号,如果向JVM中某个程序注入了一个Hook线程,则JVM咋进程退出的时候,Hook线程会启动执行,例如
public class TestHook {
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(){
@Override
public void run() {
System.out.println("The Hook Thread 1 is running.");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("The Hook Thread 1 exit.");
}
});
Runtime.getRuntime().addShutdownHook(new Thread(){
@Override
public void run() {
System.out.println("The Hook Thread 2 is running.");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("The Hook Thread 2 exit.");
}
});
System.out.println("The program will is stopping");
}
}
会发现当主线程结束的时候,也就是说明了JVM中没有了活动的非守护线程,JVM即将退出的时候,两个Hook线程就会被执行。
Hook实战
在实际开发中也会进程使用到Hook线程,为了防止程序重复启动,所以在启动时会创建一个lock文件,进程中收到中断的信号的时候会删除该文件,在很多的地方都是有这样的一个机制存在,例如Zookeeper、kafka等系统中。
public class Proctected {
private final static String LOCK_PAHT = "./";
private final static String LOCK_FILE = ".lock";
private final static String PERMISSIONS = "rw-------";
public static void main(String[] args) throws IOException {
Runtime.getRuntime().addShutdownHook(new Thread(()->{
System.out.println("The program received kill SIGNAL.");
getLockFile().toFile().delete();
}));
checkRunning();
for (;;){
try {
TimeUnit.MILLISECONDS.sleep(1);
System.out.println("program is running");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private static void checkRunning() throws IOException {
Path path = getLockFile();
if (path.toFile().exists()){
throw new RuntimeException("The program already running.");
}
Set<PosixFilePermission> perms = PosixFilePermissions.fromString(PERMISSIONS);
Files.createFile(path,PosixFilePermissions.asFileAttribute(perms))
}
private static Path getLockFile() {
return Paths.get(LOCK_FILE,LOCK_FILE);
}
}
执行上面程序在当前文件路径下面会多出一个.lock文件,如果结束进程之后就删除.lock文件。
注意
- Hook线程只会在收到退出信号的时候才会被执行,如果是强制中断的话则不会被执行。
- Hook线程中可以执行的操作有关闭文件句柄、释放数据库连接、释放网络连接等等操作
- 尽量避免在Hook线程中执行比较耗时的操作,这会导致线程不能退出。
总结
本次分享就两点内容,一是线程异常处理,另一个是Hook线程,分析了线程异常执行的顺序。分析了Hook线程使用的注意事项。