7.1 获取线程运行时异常
在Thread类中,关于处理运行时异常的API总共有四个,如下:
public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
checkAccess();
uncaughtExceptionHandler = eh;
}
.................
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(
new RuntimePermission("setDefaultUncaughtExceptionHandler")
);
}
defaultUncaughtExceptionHandler = eh;
}
........................
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
..........................
public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler(){
return defaultUncaughtExceptionHandler;
}
7.1.1 UncaughtExceptionHandler的介绍
线程在执行单元中是不允许抛出checked异常的,而且线程运行在自己的上下文中,派生它的线程将无法直接获得它运行中出现的异常信息。对此,Java为我们提供了一个UncaughtExceptionHandler接口,当线程运行过程中出现异常时,会回调UncaughtException接口,从而得知是哪个线程在运行时出错,以及出现了什么样的错误。
public class CaptureThreadException {
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 e1) {
e1.printStackTrace();
}
System.out.println(1/0);
},"Test-Thread");
thread.start();
}
}
......................
java.lang.ArithmeticException: / by zero
Test-Threadoccur exception
at Test/cn.yu.uncaughtexceptionhandler.CaptureThreadException.lambda$1(CaptureThreadException.java:19)
at java.base/java.lang.Thread.run(Thread.java:835)
7.1.2 UncaughtExceptionHandler源码分析
Thread类的源码
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
getUncaughtExceptionHandler方法首先会判断当前线程是否设置了Handler,如果有则执行线程自己的uncaughtException方法,否则就到所在的ThreadGroup中获取,ThreadGroup同样也实现了UncaughtExceptionHandler接口
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);
}
}
}
1.该ThreadGroup如果有父ThreadGroup,则直接调用父Group的uncaughtException方法
2.如果设置了全局默认的UncaughtExceptionHandler,则调用uncaughtException方法
3.若既没有父HTreadGroup,也没有设置全局默认的UncaughtExceptionHandler,则会直接将异常的堆栈信息定向到System.err中
7.2 注入钩子线程
7.2.1 Hook线程介绍
JVM进程的退出是由于进程中没有活跃的非守护线程,或者受到了系统中断信号,想JVM程序注入一个HooK线程,在JVM进程退出的时候,Hook线程会启动执行,通过向Runtime可以为JVM注入多个Hook线程
public class ThreadHook {
public static void main(String[] args) {
//钩子线程1
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
System.out.println("The hook thread 1 is running");
try {
TimeUnit.MICROSECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("The hook thread 1 will exit.");
}
});
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
System.out.println("The hook thread 2 is running");
try {
TimeUnit.MICROSECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("The hook thread 2 will exit.");
}
});
System.out.println("The program will is stopping.");
}
}
...............................
The program will is stopping.
The hook thread 1 is running
The hook thread 2 is running
The hook thread 2 will exit.
The hook thread 1 will exit.
在上述代码中,给Java程序注入了两个Hook线程,在main线程中结束,也就是JVM中没有了活动的非守护线程,JVM进程即将退出时,两个Hook线程会被启动并运行。
7.2.2 Hook线程实战
public class PreventDuplicated {
private final static String Lock_Path = "/Test/";
private final static String LOCK_FILE = ".lock";
private final static String PERMISSIONS = "rw-----------";
public static void main(String[] args) throws InterruptedException, IOException {
//注入Hook线程,在程序退出时,删除lock文件
Runtime.getRuntime().addShutdownHook(new Thread(()->{
System.out.println();
getLockFile().toFile().delete();
}) );
//检查是否存在.lock文件
checkRunning();
//简单模拟当前程序正在运行
for(;;) {
TimeUnit.MILLISECONDS.sleep(1);
System.out.println("程序正在运行");
}
}
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_Path, LOCK_FILE);
}
}
7.2.3 Hook线程应用场景以及注意事项
1.Hook线程只有在收到退出信号的时候会被执行,如果在kill的时候使用了参数-9,那么Hook线程不会得到执行,进程将会立即退出,一次.lock文件将得不到清理
2.Hook线程中也可以执行一些资源释放的工作,比如关闭文件句柄,socket链接,数据库connection等
3.尽量不要在Hook线程中执行一些耗时非常长的操作,因为其会导致导致程序迟迟不能退出。
本章总结:
本章介绍了通过 UncaughtExceptionHandler回调的方式获取线程运行期间的异常信息。
Hook线程是一个非常好的机制,以帮助程序获得进程中断信号,有机会在进程退出之前做一些资源释放的动作,或者做一些告警通知。切记如果强制杀死进程,那么进程将不会收到任何中断信号。