新来的同事问我当进程/机器突然停止时,finally 到底会不会执行?

欢迎大家继续阅读 新来的同事原创问题系列 ,历史好文:

新来的同事问我where 1=1 是什么意思

新来的同事问我 ON DUPLICATE KEY 是什么意思

新来的同事问我 怎么没有mapper.xml文件

这不,又新来了一批同事,问我当进程/机器突然停止时,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 会执行,什么时候不会?

  1. 语言语义 vs 操作系统控制权
    finally 是 Java 的语言语义 ------ JVM 在栈展开时去执行。操作系统可以在任意时刻剥夺进程运行权(kill -9、断电),这时语言语义失效,因为程序根本没被允许"干最后一件事"。

  2. 有序关机 vs 强制终止

    • 有序关机(System.exit、SIGTERM)会给 JVM 机会去执行 shutdown hooks(注意:不一定会到触发 System.exit() 的 finally)。所以第二段demo中使用了System.exit(0),JVM启动关机流程,通常不会打印 finally 内的内容

    • 强制终止(SIGKILL、断电、内核 panic、JVM 本身 crash)没有任何"优雅收尾"。

  3. JVM 崩溃和本地代码

    当 JVM 的本地层(C/C++ JNI、本地内存)出现问题导致 crash,Java 层无能为力。你可能会看到 hs_err 文件,但 Java 清理代码无法运行。

  4. 异常并不等于崩溃

    抛出 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?在评论里说说你们遇到的宕机事故吧!

相关推荐
步行cgn3 分钟前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
y1su19 分钟前
Leetcode 字符串模板
java·数据结构·算法·leetcode·职场和发展·动态规划
码事漫谈1 小时前
AI 读你的提示词,其实很没耐心
后端
用户8356290780512 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我123452 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒2 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记2 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端
Mk598782 小时前
P4:把 Agent 的记忆搬进 Redis —— AgentScope-Java 工程化实战
后端
雨中飘荡的记忆2 小时前
Claude Code 入门必会:@、!、Slash Commands,一篇讲清楚
人工智能·后端
苜蓿muxu2 小时前
Java 字符串拼接:+、StringBuilder、StringBuffer的区别
java