新来的同事问我当进程/机器突然停止时,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. 异常并不等于崩溃

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

相关推荐
板板正37 分钟前
SpringAI——向量存储(vector store)
java·spring boot·ai
野生技术架构师43 分钟前
Spring Boot 定时任务与 xxl-job 灵活切换方案
java·spring boot·后端
苹果醋32 小时前
Java并发编程-Java内存模型(JMM)
java·运维·spring boot·mysql·nginx
你怎么知道我是队长2 小时前
C语言---编译的最小单位---令牌(Token)
java·c语言·前端
Elieal2 小时前
Java 链表完全指南:从基础到力扣简单题实战
java·leetcode·链表
寒士obj3 小时前
SpringBoot中的条件注解
java·spring boot·后端
pengzhuofan3 小时前
Java设计模式-外观模式
java·设计模式·外观模式
Emrys_3 小时前
AQS 深入解析
java
G探险者3 小时前
循环中的阻塞风险与异步线程解法
后端
易元3 小时前
模式组合应用-桥接模式(二)
后端