1、捕获线程运行时异常
Thread 类中,关于处理运行时异常的 API 总共有四个,如下:
// 为某个特定线程指定 UncaughtExceptionHandler
public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
checkAccess();
uncaughtExceptionHandler = eh;
}
// 设置全局的 UncaughtExceptionHandler
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(
new RuntimePermission("setDefaultUncaughtExceptionHandler")
);
}
defaultUncaughtExceptionHandler = eh;
}
// 获取特定线程的 UncaughtExceptionHandler
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
// 获取全局的 UncaughtExceptionHandler
public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler(){
return defaultUncaughtExceptionHandler;
}
1.1、UncaughtExceptionHandler 介绍
线程在执行单元中是不允许抛出 checked 异常的,而且线程运行在自己的上下文中,派生它的线程将无法直接获得它运行中出现的异常信息。对此,Java 为我们提供了一个 UncaughtExceptionHandler 接口,当线程在运行过程中出现异常时,会调用 UncaughtExceptionHandler 接口,从而得知是哪个线程在运行时出错,以及出现了什么样的错误。 Thread 中 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);
}
从上面源码中可以看出 UncaughtExceptionHandler 是一个FunctionalInterface(该注解只能标记在**的接口上) 接口,只有一个抽象方法。该回调接口会被 Thread 中的 dispatchUncaughtException 发方法调用,代码如下。当线程在运行过程中出现异常时,JVM 会调用 dispatchUncaughtException 方法,该方法会将对应的线程实例以及异常信息传递给回调接口。
/**
* 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);
}
1.2、UncaughtExceptionHandler 实例
示例代码:
package com.yj.thread_group;
import java.util.concurrent.TimeUnit;
/**
* @description: UncaughtExceptionHandler 实例
* @author: erlang
* @since: 2021-03-01 22:02
*/
public class ThreadUncaughtExceptionHandler {
public static void main(String[] args) {
// 设置回调接口
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
System.out.println(t.getName() + " throw exception");
e.printStackTrace();
});
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
}
System.out.println(1 / 0);
}, "Thread-Demo").start();
}
}
执行结果:
Thread-Demo throw exception
java.lang.ArithmeticException: / by zero
at com.yj.thread_group.UncaughtExceptionHandlerDemo.lambda$main$1(UncaughtExceptionHandlerDemo.java:26)
at java.lang.Thread.run(Thread.java:748)
1.3、UncaughtExceptionHandler 源码
在没有向线程注入 UncaughtExceptionHandler 回调接口的情况时,线程若出现了异常有将如何处理呢?从 getUncaughtExceptionHandler 方法可以看出,该方法首先会判断当前线程是否设置了 handler?如果有则执行线程自己实现的 uncaughtException 方法,否则就到所在的 ThreadGroup 中获取。
// 获取特定线程的 UncaughtExceptionHandler
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
ThreadGroup 中 UncaughtExceptionHandler 接口的实现,代码如下:
- 如果该线程组有父线程组,则直接调用父线程组的 uncaughtException 方法;
- 如果设置了全局默认的 UncaughtExceptionHandler,则调用该 Handler 的 uncaughtException 方法
- 如果既没有父线程组,也没有设置全局线默认的 UncaughtExceptionHandler,则会直接将异常的堆栈信息定向到 System.err 中。
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);
}
}
}
2、注入钩子线程
2.1、Hook 线程
JVM 进程的退出是由于 JVM 进程汇中没有活跃的非守护线程,或者收到了系统中断信号,向 JVM 程序注入一个 Hook 线程,在 JVM 进程退出的时候,Hook 线程会启动执行,通过 Runtime 可以为 JVM 注入多个线程。示例代码:
package com.yj.thread_group;
import java.util.concurrent.TimeUnit;
/**
* @description: 钩子线程
* @author: erlang
* @since: 2021-03-02 22:07
*/
public class HookThread {
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("The hook thread1 is start.");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("The hook thread1 is end.");
}));
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("The hook thread2 is start.");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("The hook thread2 is end.");
}));
System.out.println("main is end");
}
}
上述代码中,给 Java 程序注入了两个 Hook 线程,在 main 线程中结束,即 JVM 中没有了活动的守护线程,JVM 进程即将退出时,两个 Hook 线程会启动运行。执行结果:
main is end
The hook thread2 is start.
The hook thread1 is start.
The hook thread2 is end.
The hook thread1 is end.
2.2、Hook 线程实战
开发中经常遇到 Hook 线程,比如为了防止某个程序被重复启动,在进程启动时会创建一个 lock 文件;在进程收到中断信号时,会删除 lock 文件。在 mysql、zookeeper 等系统中都能看到这个 lock 文件。示例代码:
package com.yj.thread_group;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @description: 启动时钩子,防止线程多次启动
* @author: erlang
* @since: 2021-03-02 22:26
*/
public class StartupHook {
private final static String LOCK_PATH = "src/main/java/locks";
private final static String LOCK_FILE = ".lock";
private final static String PERMISSION = "rwxr-xr-x";
public static void main(String[] args) throws InterruptedException, IOException {
// 检查是否存在 .lock 文件
checkRunning();
// 注入 Hook 线程,在程序退出时删除 lock 文件
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("\nThe hook is start.");
getLockFile().toFile().delete();
}));
// 模拟程序启动中
System.out.println("running ");
for (;;) {
TimeUnit.SECONDS.sleep(1);
System.out.print("#");
}
}
private static Path getLockFile() {
return Paths.get(LOCK_PATH, LOCK_FILE);
}
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(PERMISSION);
Files.createFile(path, PosixFilePermissions.asFileAttribute(perms));
}
}
运行上面的程序,会发现 LOCK_PATH 目录下多了一个 .lock 文件,如图所示。
终止程序时,JVM 进程会收到中断信号,并启动 hook 线程,删除 .lock 文件,执行结果如下:
running
######
The hook is start.
3、Hook 线程应用场景
Hook 线程只有在收到退出信号的时候会被执行,如果在 kill 的时候使用了参数 -9,那么 Hook 线程不会得到执行,进程将会立即退出,.lock 文件将不会被删除。Hook 线程中也可以执行一些释放资源的工作,比如关闭文件句柄、socket 链接、数据库链接等。尽量不要在 Hook 线程中执行一些耗时非常长的操作,因为这会导致程序迟迟不能退出
4、总结
本篇介绍了如何通过 Handler 回调的方式获取线程运行期间的异常信息。Hook 线程是一个非常好的机制,可以帮助程序获得进程的中断信号,有机会在进程退出之前做一些资源释放的动作,或者做一些告警通知。如果是强制杀死进程(kill -9),那么进程将不会收到任何中断信号。