欢迎大家继续阅读 新来的同事原创问题系列
,历史好文:
新来的同事问我 ON DUPLICATE KEY 是什么意思

这不,又新来了一批同事,问我当进程/机器突然停止时,finally 到底会不会执行?,那咱今天继续唠唠
疑问抛出
同事看完日志讲道:finally 不是保证在 try 后一定会运行吗?为什么我在日志里没有看到 finally 的打印?我笑了,今天就来讲清楚 finally 在不同"宕机/终止"场景下的行为,并给出可落地的防护策略

先来看两段demo
java
// FinallyDemo.java
public class FinallyDemo {
public static void main(String[] args) {
try {
System.out.println("try start");
if (true) throw new RuntimeException("boom");
// return; // 也可以把上面换成 return 测试
} finally {
System.out.println("finally executed");
}
}
}
java
// SystemExitDemo.java
public class SystemExitDemo {
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("shutdown hook running");
}));
try {
System.out.println("in try");
System.exit(0);
} finally {
System.out.println("in finally");
}
}
}
上面两段代码是都会走finally, 亦或是一个走、一个不走呢
finally 到底是什么?为什么它很"可靠"?
finally
的语义很直接:当方法退出(无论是正常返回还是抛出异常导致的栈展开)时,JVM 会在栈展开过程中执行 finally
块。把它想像成:当你离开房间(方法返回/异常),你按下"关门"的那个动作 ------ finally
就是关门动作里的清理步骤。
这也是为什么我们常把 finally
用来释放资源(关闭流、释放锁等):在大多数正常错误/异常流里,它的确是最后的防线。可是为什么看不到finally日志输出呢
什么时候 finally
会执行,什么时候不会?
-
语言语义 vs 操作系统控制权
finally
是 Java 的语言语义 ------ JVM 在栈展开时去执行。操作系统可以在任意时刻剥夺进程运行权(kill -9
、断电),这时语言语义失效,因为程序根本没被允许"干最后一件事"。 -
有序关机 vs 强制终止
-
有序关机(
System.exit
、SIGTERM)会给 JVM 机会去执行 shutdown hooks(注意:不一定会到触发System.exit()
的finally
)。所以第二段demo中使用了System.exit(0),JVM启动关机流程,通常不会打印finally
内的内容 -
强制终止(SIGKILL、断电、内核 panic、JVM 本身 crash)没有任何"优雅收尾"。
-
-
JVM 崩溃和本地代码
当 JVM 的本地层(C/C++ JNI、本地内存)出现问题导致 crash,Java 层无能为力。你可能会看到 hs_err 文件,但 Java 清理代码无法运行。
-
异常并不等于崩溃
抛出
OutOfMemoryError
、RuntimeException
等 ------ 在 JVM 仍健康时,栈展开会走完,finally
会跑。但极端内存耗尽可能让 JVM 处于不稳定状态,从而导致不可预测结果。所以刚才给的demo,第一段会进finally
所以:
finally
的可靠性是"在 JVM 正常控制下"
的可靠;遇到"外力直接剥夺控制权"的情况,它失效。
所以总结如下:三种会执行,三种不会执行
finally
会执行(大概率)
- 方法里抛异常(受 JVM 控制)并向上传播 ------ JVM 在栈展开期间会执行
finally
。 - 方法正常返回(包括
return
) ------finally
会在返回之前执行。 ThreadDeath
(由Thread.stop()
引发)理论上会触发栈展开,从而执行finally
(但不推荐使用Thread.stop()
)。
finally
不会执行(或不保证执行)
kill -9
(SIGKILL)或等效的强制终止 :内核直接移除进程,JVM 没机会执行任何 Java 代码,包括finally
和 shutdown hooks。- 机器断电 / 主机崩溃 / 内核 panic:硬件或内核层面直接停止,一切都结束。
- 在
try
中调用System.exit()
:JVM 会启动关机流程(执行 shutdown hooks),但不会回到触发System.exit()
的代码路径继续执行其finally
(通常不会打印finally
内的内容)。
不要把关键保证"只"交给 finally
看完刚才的叙述我们应该知道,这种非百分百的事情,还是尽量不要碰。下面我给大家汇总了几条建议,生产环境这么搞,总监都会夸你

-
关键操作放在可靠持久化系统
把关键事件(交易状态、任务完成标记)写入数据库或 Kafka 等有持久保障的系统,而不是仅仅依赖内存或内存中的
finally
。 -
写磁盘要走"临时文件 → fsync → 原子重命名"流程
换句话说:写入先写
.tmp
文件,FileChannel.force(true)
,最后Files.move(..., ATOMIC_MOVE)
。这样断电或中断后可以检测到未完成的.tmp
文件并恢复。 -
幂等与重试
后端重启后要能安全重做操作。用 idempotency key(唯一事务 ID)标记操作,避免重复副作用。
-
使用 shutdown hook 做最后努力,但别指望它能对抗
SIGKILL
addShutdownHook
用于有序关机收尾(关闭连接、短持久化)。但 hooks 必须短小可靠,不该做长事务或复杂阻塞操作。 -
外部监督 + 恢复流程
使用 Supervisor / systemd / Kubernetes(带就绪探针和重启策略)来重启进程,并在启动时检查上次未完成的事务并恢复。
-
硬件层面的保障(如果业务强一致性必须)
使用 UPS、电池供电、或冗余写入来降低掉电风险。
-
测试:模拟 SIGTERM、SIGKILL、OOM、JVM crash 的恢复
写自动化/集成测试,验证系统在这些极端场景下的恢复能力。
实战代码模版(可参考)
java
import java.nio.file.*;
import java.nio.channels.FileChannel;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class RobustWorker {
private final Path checkpoint = Paths.get("checkpoint.json");
private volatile boolean running = true;
private int progress = 0;
public static void main(String[] args) throws Exception {
RobustWorker w = new RobustWorker();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("[hook] shutdown hook: try persist progress");
w.persistProgress();
}));
w.run();
}
public void run() {
try {
while (running && progress < 100) {
doWorkChunk();
progress++;
if (progress % 10 == 0) {
persistProgress(); // 定期持久化
}
}
} finally {
// 局部资源释放(例如关闭连接、停止线程池)
System.out.println("[finally] release local resources");
releaseResources();
}
}
private void doWorkChunk() {
try {
// 模拟工作(实际业务逻辑)
Thread.sleep(200);
System.out.println("work done chunk: " + progress);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
running = false;
}
}
public synchronized void persistProgress() {
try {
String content = "{\"progress\":" + progress + "}";
Path tmp = Paths.get(checkpoint.toString() + ".tmp");
Files.write(tmp, content.getBytes(StandardCharsets.UTF_8),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
try (FileChannel ch = FileChannel.open(tmp, StandardOpenOption.WRITE)) {
ch.force(true); // fsync
}
Files.move(tmp, checkpoint, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
System.out.println("[persist] progress=" + progress);
} catch (Throwable t) {
// Shutdown hook 里不要抛异常
System.err.println("[persist] failed: " + t.getMessage());
}
}
private void releaseResources() {
// 实际场景里要关闭连接、线程池等
}
}
你们的服务有没有把关键数据只放在 finally?在评论里说说你们遇到的宕机事故吧!