Unchecked 异常
什么是 unchecked 异常
在 Java 中有形形色色的异常。如果要长篇大论的话,那么就如下段:
Throwable 包括 Error 和 Exception。
Error 包括 LinkeageError、VirtualMachineError ...
Exception 包括 RuntimeException、IOException、AWTException ...
这里面我们常见的是 Exception,大多数 Exception 是编译器可以发现的,这种叫做 Checked 异常。在 Java 入门时介绍过try ... exception() ...
这样的语法了。
但是 RuntimeException 并不能在编译时被发现。要运行起来后才能被发现,最常见的是下标溢出、空指针。
RuntimeException 和 Error,统称 Unchecked 异常。
常规的方法无法捕捉
我们写下面一段代码,内容有除以0,会抛出一个 Runtime 异常。
public static void main(String[] args) {
Runnable runnable = () -> {
System.out.print(10/5);
System.out.print(10/0);
};
Thread thread = new Thread(runnable);
thread.start();
}
如果就这样允许的话,我们可以在终端看到异常
Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
我们试试用之前的try ... catch
来捕获。
public static void main(String[] args) {
Runnable runnable = () -> {
System.out.print(10/5);
System.out.print(10/0);
};
try {
Thread thread = new Thread(runnable);
thread.start();
} catch (Exception e) {
System.out.print("捕获到了异常"); // 并不会输出这一句
}
}
其结果是,还是和刚才一样,输出了异常,而不是输出我们设定的字符串。这说明,线程中的Runtime 异常并不会被普通的try ... catch ...
捕获。
Thread的run方法是不抛出任何检查型异常的,但是它自身却可能因为一个异常而被中止,导致这个线程的终结。线程不可以直接抛出异常,当出现 Unchecked 异常时,需要回调 UncaughtExceptionHandler 接口。
UncaughtExceptionHandler 接口
我们可以用 Thread
对象的setUncaughtExceptionHandler()
来设置一个异常处理器。
public static void main(String[] args) {
Runnable runnable = () -> {
System.out.print(10/5);
System.out.print(10/0);
};
Thread thread = new Thread(runnable);
thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println(t.getName() + " 线程中发生了 " + e.toString());
}
});
thread.start();
}
输出
2Thread-0 线程中发生了 java.lang.ArithmeticException: / by zero
这里也可以把代码优化为 lambda 表达式。
public static void main(String[] args) {
Runnable runnable = () -> {
System.out.print(10/5);
System.out.print(10/0);
};
Thread thread = new Thread(runnable);
thread.setUncaughtExceptionHandler((t, e) -> System.out.println(t.getName() + " 线程中发生了 " + e.toString()));
thread.start();
}
Android 系统中的例子
/**
* Handle application death from an uncaught exception. The framework
* catches these for the main threads, so this should only matter for
* threads created by applications. Before this method runs, the given
* instance of {@link LoggingHandler} should already have logged details
* (and if not it is run first).
*/
private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
private final LoggingHandler mLoggingHandler;
public KillApplicationHandler(LoggingHandler loggingHandler) {
this.mLoggingHandler = Objects.requireNonNull(loggingHandler);
}
@Override
public void uncaughtException(Thread t, Throwable e) {
try {
// Don't re-enter -- avoid infinite loops if crash-reporting crashes.
if (mCrashing) return;
mCrashing = true;
if (ActivityThread.currentActivityThread() != null) {
ActivityThread.currentActivityThread().stopProfiling();
}
// step1
// Bring up crash dialog, wait for it to be dismissed
ActivityManager.getService().handleApplicationCrash(
mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
} catch (Throwable t2) {
if (t2 instanceof DeadObjectException) {
// System process is dead; ignore
} else {
try {
// step2
Clog_e(TAG, "Error reporting crash", t2);
} catch (Throwable t3) {
// Even Clog_e() fails! Oh well.
}
}
} finally {
// step 3
// Try everything to make sure this process goes away.
Process.killProcess(Process.myPid());
System.exit(10);
}
}
}
第一步,弹出crash的对话框;
第二步:crash日志输出;
第三步,在finally 里面杀掉进程退出。
Hook 线程
印象里在初中学 Visual Basic 时有什么“程序退出时”的事件。在这样的事件中,写一些“善后”的代码,比如保存游戏进度等。
在 Java 里,这样的功能由 Hook 线程实现。
使用Runtime.getRuntime().addShutdownHook()
来添加一个 hook 线程,在进程即将退出时,会运行这些 hook 线程。
触发条件
笼统地讲,就是“JVM 进程即将退出时”。仔细地讲,则包括以下两种情况
没有 active 的非守护线程;
中断;
满足二者任意之一即可。
多个 Hook 线程
可以设置多个 Hook 线程。会存放在一个映射IdentityHashMap<Thread, Thread> hooks
之中。这种数据结构判断 key 是否相同是并不是使用equals
方法,而是看引用的是否是同一对象。
当触发条件、进程即将退出时,会运行这个 Map 中所有的线程。
简单示例
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hook 线程被调用");
}
}));
// 装模作样做一些计算
int a = 1;
int b = 1;
for (int i = 0; i < 10; i++) {
int temp = a + b;
a = b;
b = temp;
System.out.println(a + ", " + b);
}
}
代码中装模作样地计算斐波那契数列,在计算完毕后,即将退出的时候,才会输出“Hook 线程被调用”。
可以看到,我这里的 Hook 线程虽然写在了装模作样计算的前面,但是实际上是退出时被调用,输出的内容在最后面。
应用场景
释放资源
那自然是释放掉文件读写的锁、数据库的连接等资源。
防止进程重复启动
在硬盘上存一个 lock 文件。
进程启动时,判断一下 lock 文件在不在,在的话就不启动;不在的话就顺利启动,但是创建 lock 文件。
进程退出时,删除掉这个 lock 文件,这个时候就需要 Hook 线程了。