文章目录
1 获取线程运行时异常
在Thread类中:,关于处理运行时异常的API总共有四个:
/**
为某个特定线程指定UncaughtExceptionHandler
**/
public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh);
/**
设置全局的UncaughtExceptionHandler
**/
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh);
/**
获取特定线程的UncaughtExceptionHandler
**/
public UncaughtExceptionHandler getUncaughtExceptionHandler();
/**
获取全局的UncaughtExceptionHandler。
**/
public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler();
1.1 UncaughtExceptionHandler接口介绍和实现
1.1.1 UncaughtExceptionHandler接口介绍
**线程在执行单元中是不允许抛出checked异常的,而且线程运行在自己的上下文中,派生它的线程将无法直接获得它运行中出现的异常信息。**对此,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);
}
UncaughtExceptionHandler是一个FunctionalInterface,只有一个抽象方法,该回调接口会被Thread中的dispatchUncaughtException方法调用:
/**
* Dispatch an uncaught exception to the handler. This method is
* intended to be called only by the JVM.
* 是个私有方法,会被jvm调用,使用者是无法调用的,该方法会将对应的线程实例以及异常信息传递给回调接口。
*/
private void dispatchUncaughtException(Throwable e) {
getUncaughtExceptionHandler().uncaughtException(this, e);
}
1.1.2 UncaughtExceptionHandler接口示例
public class CaptureThreadExceptionTest {
public static void main(String[] args) {
// 设置默认的异常处理器
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
System.out.println(t.getName() + " 发生了异常:" + e.getMessage());
// 打印异常的堆栈信息
e.printStackTrace();
});
new Thread(() -> {
// 模拟出现异常
System.out.println(1 / 0);
}, "test-thread").start();
}
}
运行结果:
test-thread 发生了异常:/ by zero
java.lang.ArithmeticException: / by zero
at study.wyy.thread.hook.CaptureThreadExceptionTest.lambda$main$1(CaptureThreadExceptionTest.java:19)
at java.lang.Thread.run(Thread.java:748)
在平时的工作中,这种设计方式是比较常见的,尤其是那种异步执行方法,比如Google的guava toolkit就提供了EventBus,在EventBus中事件源和实践的subscriber两者借助于EventBus实现了完全的解耦合,但是在subscriber执行任务时有可能会出现异常情况,EventBus同样也是借助于一个ExceptionHandler进行回调处理的。
1.1.3 UncaughtExceptionHandler源码分析
- 在没有向线程注入UncaughtExceptionHandler回调接口的情况下,线程若出现了异常又将如何处理呢?
这个只有Thread的源码会告诉我们答案:
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
// 如果线程本身自己的uncaughtExceptionHandler 为空,就返回当前线程所属的线程组(可见线程组默认是实现了UncaughtExceptionHandler接口)
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
// 线程组默认是实现了UncaughtExceptionHandler接口
class ThreadGroup implements Thread.UncaughtExceptionHandler
getUncaughtExceptionHandler方法首先会判断当前线程是否设置了handler,如果有则执行线程自己的uncaughtException方法,否则就到所在的ThreadGroup中获取,Thread-Group同样也实现了UncaughtExceptionHandler接口,下面再来看看ThreadGroup的uncaught-Exception方法:
public void uncaughtException(Thread t, Throwable e) {
if (parent != null) {
// 该ThreadGroup如果有父ThreadGroup,则直接调用父Group的uncaughtException方法。
parent.uncaughtException(t, e);
} else {
// 获取线程的全局默认UncaughtExceptionHandler
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler();
if (ueh != null) {
// // 如果设置了全局默认的UncaughtExceptionHandler,则调用uncaughtException方法。
ueh.uncaughtException(t, e);
} else if (!(e instanceof ThreadDeath)) {
// 若既没有父ThreadGroup,也没有设置全局默认的UncaughtExceptionHandler,则会直接将异常的堆栈信息定向到System.err中。
System.err.print("Exception in thread \""
+ t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
}
整个的优先级:
- 如果线程本身自己的uncaughtExceptionHandler 不为空,就执行自己的uncaughtExceptionHandler,否则就去执行ThreadGroup的
- 该ThreadGroup如果有父ThreadGroup,则直接调用父Group的uncaughtException方法。
- 如果设置了全局默认的UncaughtExceptionHandler,则调用uncaughtException方法。
- 若既没有父ThreadGroup,也没有设置全局默认的UncaughtExceptionHandler,则会直接将异常的堆栈信息定向到System.err中
public class CaptureThreadExceptionTest {
public static void main(String[] args) {
// 设置默认的异常处理器
// Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
// System.out.println(t.getName() + " 发生了异常:" + e.getMessage());
// // 打印异常的堆栈信息
// e.printStackTrace();
// });
new Thread(() -> {
// 模拟出现异常
System.out.println(1 / 0);
}, "test-thread").start();
}
}
去掉了默认处理器:当出现异常的时候,test-thread会先找自己是否有异常处理器,发现没有就会去找它所属的线程组(main),发现main线程组没有父线程,就去找是是否有全局默认,也没有,最后就直接将异常的堆栈信息定向到System.err中
2 注入钩子线程
2.1 Hook线程介绍
JVM进程的退出是由于JVM进程中没有活跃的非守护线程,或者收到了系统中断信号,向JVM程序注入一个Hook线程,在JVM进程退出的时候,Hook线程会启动执行,通过Runtime可以为JVM注入多个Hook线程:
public class ThreadHook {
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
try {
System.out.println("The hook thread 1 is running.");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("The hook thread 1 will exit.");
}
});
// 钩子线程可注册多个
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
try {
System.out.println("The hook thread 2 is running.");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("The hook thread 2 will exit.");
}
});
System.out.println("The program will is stopping.");
}
}
给Java程序注入了两个Hook线程,在main线程中结束,也就是JVM中没有了活动的非守护线程,JVM进程即将退出时,两个Hook线程会被启动并且运行,运行结果:
The program will is stopping.
The hook thread 1 is running.
The hook thread 2 is running.
The hook thread 1 will exit.
The hook thread 2 will exit.
2.2 Hook线程实战
在我们的开发中经常会遇到Hook线程,比如为了防止某个程序被重复启动,在进程启动时会创建一个lock文件,进程收到中断信号的时候会删除这个lock文件,我们在mysql服务器、zookeeper、kafka等系统中都能看到lock文件的存在。
现在就简单模拟一下这个场景:
package study.wyy.thread.hook;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.TimeUnit;
/**
* @author wyaoyao
* @date 2021/4/3 11:25
*/
public class PreventDuplicatedDemo {
/**
* 锁文件位置:设置为用户的家目录
*
*/
private static final String LOCK_PATH = System.getProperty("user.home") + "/";
private final static String LOCK_FILE = ".lock";
private final static String PERMISSIONS = "rw-------";
public static void main(String[] args) throws IOException, InterruptedException {
// 注入一个hook线程
Runtime.getRuntime().addShutdownHook(new Thread(()->{
System.out.println("The program received kill signal");
// 删除文件
getLockFile().toFile().delete();
},"delete-lock-file-thread"));
// 检查程序是否运行
checkRunning();
// 创建这个文件
createFile();
// 简单模拟一个程序
for (; ; ) {
try {
TimeUnit.MILLISECONDS.sleep(1);
System.out.println("program is running.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private static void createFile() throws IOException {
Path path = getLockFile();
// PosixFilePermissions 不支持windows
// Set<PosixFilePermission> perms = PosixFilePermissions.fromString(PERMISSIONS);
// Files.createFile(path, PosixFilePermissions.asFileAttribute(perms));
Files.createFile(path);
}
/**
* 如果存在这个标记文件,就代表程序已经运行
*/
private static void checkRunning() {
Path path = getLockFile();
if (path.toFile().exists()){
// 如果这文件存在那么就抛出异常
throw new RuntimeException("The program already running.");
}
}
private static Path getLockFile()
{
return Paths.get(LOCK_PATH, LOCK_FILE);
}
}
执行kill pid或者kill-1 pid命令之后,JVM进程会收到中断信号,并且启动hook线程删除.lock文件.
2.3 Hook线程应用场景以及注意事项
- Hook线程只有在收到退出信号的时候会被执行,如果在kill的时候使用了参数-9,那么Hook线程不会得到执行,进程将会立即退出,因此.lock文件将得不到清理。
- Hook线程中也可以执行一些资源释放的工作,比如关闭文件句柄、socket链接、数据库connection等。
- 尽量不要在Hook线程中执行一些耗时非常长的操作,因为其会导致程序迟迟不能退出。