1.获取线程运行时异常
在Thread类中,关于处理运行时异常的API总共有四个,如下所示:
public void set UncaughtExceptionHandler(UncaughtExceptionHandler eh)
//为某个特定线程指定UncaughtExceptionHandler。
public static void set DefaultUncaughtExceptionHandler(UncaughtExceptionHandler
eh)
//设置全局的UncaughtExceptionHandler。
public UncaughtExceptionHandler getUncaughtExceptionHandler()
//获取特定线程的UncaughtExceptionHandler。
1.1 UncaughtExceptionHandler的介绍
线程在执行单元中是不允许抛出checked异常的,这一点前文中已经有过交代,而且线 程运行在自己的上下文中,派生它的线程将无法直接获得它运行中出现的异常信息。对此,Java为我们提供了一个UncaughtExceptionHandler接口,当会回线程在运行调Uncaught过程中出Exception现异常Handler时,接口, 从而得知是哪个线程在运行时出错,以及出现了什么样的错误,示例代码如 下:
@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::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() {
@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 whill 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线程会被启动并且运行,输出结果如下:
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线程中执行一些耗时非常长的操作, 因为其会导致程序迟迟不能退出。