1.获取线程运行时异常
在Thread类中,关于处理运行时异常的API总共有四个,如下所示:
public void set UncaughtExceptionHandler(UncaughtExceptionHandler eh)//为某个特定线程指定UncaughtExceptionHandler。public static void set DefaultUncaughtExceptionHandler(UncaughtExceptionHandlereh)//设置全局的UncaughtExceptionHandler。public UncaughtExceptionHandler getUncaughtExceptionHandler()//获取特定线程的UncaughtExceptionHandler。
1.1 UncaughtExceptionHandler的介绍
线程在执行单元中是不允许抛出checked异常的,这一点前文中已经有过交代,而且线 程运行在自己的上下文中,派生它的线程将无法直接获得它运行中出现的异常信息。对此,Java为我们提供了一个UncaughtExceptionHandler接口,当会回线程在运行调Uncaught过程中出Exception现异常Handler时,接口, 从而得知是哪个线程在运行时出错,以及出现了什么样的错误,示例代码如 下:
@FunctionalInterfacepublic 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::uncaughtException方法会被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);}
当线程在运行过程中出现异常时,JVM会调用dispatchUncaughtException方法,该方法会将对应的线程实例以及异常信息传递给回调接口。
1.2 UncaughtExceptionHandler实例
import java.util.concurrent.TimeUnit;public class TestThreadException {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 e) {e.printStackTrace();}System.out.println(1/0);},"Test-Thread");thread.start();}}
1.3 UncaughtExceptionHandler 源码分析
public UncaughtExceptionHandler getUncaughtExceptionHandler() {return uncaughtExceptionHandler != null ?uncaughtExceptionHandler : group;}
getUncaughtExceptionHandler方法首先会判断当前线程是否设置了handler, 如果有则执行线程自己的uncaught Exception方法, 否则就到所在的ThreadGroup中获取,ThreadGroup同样也实现了UncaughtExceptionHandler接口, 下面再来看看Thread Group的uncaughtException方法。
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);}}}
该ThreadGroup如果有父ThreadGroup, 则直接调用父Group的uncaughtException方法。如果设置了全局默认的UncaughtExceptionHandler, 则调用uncaughtException方法。若既没有父ThreadGroup,也没有设置全局默认的UncaughtExceptionHandler, 则会直接将异常的堆栈信息定向到System.err中。
2.注入钩子线程
2.1Hook线程介绍
JVM进程的退出是由于JVM进程中没有活跃的非守护线程, 或者收到了系统中断信号, 向JVM程序注入一个Hook线程, 在JVM进程退出的时候, Hook线程会启动执行,通过Runtime可以为JVM注入多个Hook线程, 下面就通过一个简单的例子来看一下如何向Java程序注入Hook线程。
import java.util.concurrent.TimeUnit;public class TestThreadHook {public static void main(String[] args) {Runtime.getRuntime().addShutdownHook(new Thread() {@Overridepublic 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 whill exit");}});Runtime.getRuntime().addShutdownHook(new Thread() {@Overridepublic 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线程会被启动并且运行,输出结果如下:
2.2Hook线程实战
在我们的开发中经常会遇到Hook线程, 比如为了防止某个程序被重复启动, 在进程启动时会创建一个lock文件, 进程收到中断信号的时候会删除这个lock文件, 我们在mysql服务器、zookeeper、kafka等系统中都能看到lock文件的存在, 本节中, 将利用hook线程的特点,模拟一个防止重复启动的程序,如代码所示。
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;public class PreventDuplicated {private final static String LOCK_PATH="D:\\locks";private final static String LOCK_FILE=".lock";private final static String PERMISSIONS="rw-------";public static void main(String[] args) throws IOException {// 注入Hook线程,在程序退出时删除lock文件Runtime.getRuntime().addShutdownHook(new Thread(()-> {System.out.println("Thre program received kill SIGNAL");}));// 检查是否存在.lock文件checkRunning();// 简答模拟当前程序正在运行for(;;) {try {TimeUnit.MILLISECONDS.sleep(1);System.out.println("program is running");} catch (InterruptedException e) {e.printStackTrace();}}}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);}}
2.3 Hook线程应用场景以及注意事项
- Hook线程只有在收到退出信号的时候会被执行, 如果在kill的时候使用了参数-9,那么Hook线程不会得到执行, 进程将会立即退出, 因此.lock文件将得不到清理。
- Hook线程中也可以执行一些资源释放的工作, 比如关闭文件句柄、socket链接、数据库connection等。
- 尽量不要在Hook线程中执行一些耗时非常长的操作, 因为其会导致程序迟迟不能退出。
