在Thread类中有四个关于异常处理的方法:
public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) ;为某个特定线程指定UncaughtExceptionHandler
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) ;设置全局的UncaughtExceptionHandler
public UncaughtExceptionHandler getUncaughtExceptionHandler() ;获取特定线程的UncaughtExceptionHandler
public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler();获取全局的UncaughtExceptionHandler
UncaughtExceptionHandler 介绍:
线程执行单元中是不允许抛出checked异常,而且在线程运行在自己的上下文中,派生它的线程将无法直接获得它运行出现的异常信息,对此Java为我们提供了一个UncaughtExceptionHandler接口(Thread中内部接口),当线程出现异常时会回调此接口,从而得知是哪个线程出错及什么样的错误。Thread类中源码
@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);
}
备注:在lamada表达式中已经明确了是在接口上的一种操作,并且接口只允许定义一个抽象方法,函数式接口由此而来(在函数式接口中依然可以定义普通方法和静态方法)
普通线程异常错误:
public static void main(String[] args) {
new Thread(() -> {
System.out.println(1/0);
},"th1").start();
}
Exception in thread "th1" java.lang.ArithmeticException: / by zero
at ThreadGroupPkg.Demo4.lambda$main$0(Demo4.java:6)
at java.lang.Thread.run(Thread.java:748)
回调UncaughExceptionHandler(在异常前回调,说白了就是覆写了Thread类中的UncaughException抽象方法)
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
System.out.println("线程" + t.getName() + ",occur exception");
e.printStackTrace();
});
new Thread(() -> {
System.out.println(1/0);
},"th1").start();
}
线程th1,occur exception
java.lang.ArithmeticException: / by zero
at ThreadGroupPkg.Demo4.lambda$main$1(Demo4.java:10)
at java.lang.Thread.run(Thread.java:748)
分析UncaughExceptionHandler源码:在平时的工作中这种异常也是比较常见的,尤其是在那种异步处理的机制中。在没有向线程中注入UncaughExceptionHandler回调接口的情况下,若线程出现了异常又如何处理了。
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
此方法会首先判断当前线程是否设置了handler,有则执行自身的uncaughException方法,否则就到ThreadGroup中获取,ThreadGroup同样也实现了此接口,下面来看看ThreadGroup中的uncaughException
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的uncaughException方法
2、如果没有父Group,设置了全局默认的uncaughExceptionHandler则调用uncaughtException方法,没设置则将异常信息定向到System.err中
public static void main(String[] args) {
ThreadGroup gp = Thread.currentThread().getThreadGroup();
System.out.println(gp.getParent());//java.lang.ThreadGroup[name=system,maxpri=10]
System.out.println(gp.getParent().getParent());//null
final Thread th = new Thread(() -> {
System.out.println(1/0);
},"th线程");//没有默认父group为创造该线程的父group,也就是mainGp了
th.start();
}
//group没有设置默认handler,线程出现异常会先找到父组(此处mainGp),在找出父类SystemGroup,在没有设置就定位到堆栈异常打印
Hook线程介绍:
jvm进程推迟是jvm中没有活跃的非守护线程或者收到了系统中断的信号,向jvm中注入一个Hook线程,在jvm退出时此线程会启动执行,通过runtime可以为jvm注入多个hook线程,如下:
public static void main(String[] args) {
//为程序注入钩子线程
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "启动成功");
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "准备退出");
},"钩子1"));
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "启动成功");
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "准备退出");
},"钩子2"));
System.out.println("main线程(非守护线程)准备结束");
}
----------------控制台----------------
main线程(非守护线程)准备结束
钩子1启动成功
钩子2启动成功
钩子2准备退出
钩子1准备退出
从中可以看出hook线程在jvm退出时自启动,无需调用start,在开发中经常会遇到Hook线程,比如为了防止某个程序重复启动,在进程启动时会创建一个lock文件,进程收到中断心信号时候会删除这个lock文件,我们在mysql、服务器、zookeepper、kafka等系统中都能看到这个lock文件,我们在mysql的特点,模拟一个防止重复启动的程序。
public class Demo6 {
public static final String FILE_PATH = "D:/BCD/";
public static final String FILE_NAME = "test.lock";
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("收到中断信号或者JVM退出");
File f = new File(FILE_PATH + FILE_NAME);
if(f.exists()){
f.delete();
System.out.println("删除成功");
}
},"th1"));
File newF = new File(FILE_PATH + FILE_NAME);
if(!newF.getParentFile().exists()){
newF.getParentFile().mkdirs();
}
try {
OutputStream out = new FileOutputStream(newF);
String str = "欢迎来到英雄联盟";
out.write(str.getBytes());
out.close();
for (;;){
TimeUnit.SECONDS.sleep(2);
System.out.println("程序正在进行");
}
} catch (Exception e) {
e.printStackTrace();
}
}
-------------------控制台--------------
程序正在进行
程序正在进行
程序正在进行
Disconnected from the target VM, address: '127.0.0.1:58606', transport: 'socket'
收到中断信号或者JVM退出
删除成功
}
此案例也很简单:在指定目录下创建一个文件,当系统退出时(我这里手动关闭),会删除该lock文件。
使用hook线程注意事项:
1、Hook线程也可以执行一些资源释放的工作,比如关闭文件句柄、socket连接、数据库连接
2、尽量不要做一些耗时较大的操作,因为会导致程序迟迟不能退出。