线程异常捕获和Hook线程

在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、尽量不要做一些耗时较大的操作,因为会导致程序迟迟不能退出。

                                          

猜你喜欢

转载自blog.csdn.net/qq_40826106/article/details/86490856