之前在牛客上看到一道面试题,是关于“finally子句是否总是一定会被执行”。在初学Java异常章节的时候,学习到try-catch-finally的语句,说明了finally子句的用途是用于确保安全的回收资源等,因此finally子句总是会被系统执行。而学习到并发章节的时候,找到了一个特例。章节内容见《Java编程思想》第21章-21.2.8-后台线程

如下一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class ADaemon implements Runnable {
@Override
public void run() {
try {
System.out.println("Starting ADaemon");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
System.out.println("Existing via InterruptedException");
} finally {
System.out.println("This should always run?");
}
}
}

public class DaemonsDontRunFinally {
public static void main(String[] atgs) throws Exception {
Thread t = new Thread(new ADaemon());
t.setDaemon(true);
t.start();
}
}

/* Output:
Starting ADaemon
*///:~

由于线程t被注册为后台线程,因此当执行该程序后,finally子句将不会得到执行,除非注释掉setDaemon()的调用。结合书中所述,由于非后台线程main()被终止时,后台线程会“突然”终止,即JVM会在main()退出后立即关闭所有后台进程,而不会有任何被希望出现的确认形式。故在这种情况下,finally子句将无法被执行。

2020.8.23更新,摘自《Java并发编程的艺术》

当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。因此在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。


在牛客的面试题中,一条回答表示“当在finally子句前调用System.exit(0)”时,finally子句就不会被执行。即如上后台线程示例的情况一致,由于在main()线程中退出,其退出动作发生在run()中的finally子句之前,故finally子句将不会被执行。

关于这个问题涉及到JVM的运行机制以及线程等问题,在此先留题占坑,等待后续学习到更加清晰的原因时再来补充解释。(2020.5.25)